diff --git a/packages/engine/paima-sm/src/delegate-wallet.ts b/packages/engine/paima-sm/src/delegate-wallet.ts index 244153bab..88af38e45 100644 --- a/packages/engine/paima-sm/src/delegate-wallet.ts +++ b/packages/engine/paima-sm/src/delegate-wallet.ts @@ -7,7 +7,6 @@ import { CryptoManager } from '@paima/crypto'; import type { IGetAddressFromAddressResult } from '@paima/db'; import { addressCache, - deleteAddress, deleteDelegationsFrom, getAddressFromAddress, getAddressFromId, @@ -18,6 +17,10 @@ import { updateAddress, } from '@paima/db'; +type AddressExists = { address: IGetAddressFromAddressResult; exists: true }; +type AddressDoesNotExist = { address: string; exists: false }; +type AddressWrapper = AddressExists | AddressDoesNotExist; + type ParsedDelegateWalletCommand = | { command: 'delegate'; @@ -100,53 +103,70 @@ export class DelegateWallet { throw new Error(`Invalid Signature for ${walletAddress} : ${message}`); } - public async getOrCreateNewAddress( - address: string - ): Promise<{ address: IGetAddressFromAddressResult; isNew: boolean }> { - const [exitingAddress] = await getAddressFromAddress.run({ address: address }, this.DBConn); - if (exitingAddress) return { address: exitingAddress, isNew: false }; - // create new. + public async createAddress(address: string): Promise { await newAddress.run({ address: address }, this.DBConn); const [createdAddress] = await getAddressFromAddress.run({ address }, this.DBConn); - return { address: createdAddress, isNew: true }; + return createdAddress; + } + + public async getAddress(address: string): Promise { + const [exitingAddress] = await getAddressFromAddress.run({ address: address }, this.DBConn); + if (!exitingAddress) return { address, exists: false }; + return { address: exitingAddress, exists: true }; } // Swap: - // addressA gains addressB ID - // addressB is assigned a new ID - private async swap(addressA: string, addressB: string): Promise { - await deleteAddress.run({ address: addressA }, this.DBConn); + // addressB gains addressA ID. + // addressA is assigned a new ID + private async swap( + addressA: AddressExists, + addressB: AddressDoesNotExist + ): Promise<{ + newA: AddressExists; + newB: AddressExists; + }> { await updateAddress.run( { - new_address: addressA, - old_address: addressB, + new_address: addressB.address, + old_address: addressA.address.address, }, this.DBConn ); - await newAddress.run({ address: addressB }, this.DBConn); + await newAddress.run({ address: addressA.address.address }, this.DBConn); + + return { + newA: (await this.getAddress(addressA.address.address)) as AddressExists, + newB: (await this.getAddress(addressB.address)) as AddressExists, + }; } private async validateDelegate( - fromAddress: { address: IGetAddressFromAddressResult; isNew: boolean }, - toAddress: { address: IGetAddressFromAddressResult; isNew: boolean } + fromAddress: AddressWrapper, + toAddress: AddressWrapper ): Promise { + if (!fromAddress.exists || !toAddress.exists) { + return; // both are new. + } + if (fromAddress.address.id === toAddress.address.id) { + throw new Error('Cannot delegate to itself'); + } + if (fromAddress.exists && toAddress.exists) { + throw new Error('Both A and B have progress. Cannot merge.'); + } + // Check if delegation or reverse delegation does not exist TODO. const [currentDelegation] = await getDelegation.run( { from_id: fromAddress.address.id, to_id: toAddress.address.id }, this.DBConn ); if (currentDelegation) throw new Error('Delegation already exists'); + const [reverseDelegation] = await getDelegation.run( { from_id: toAddress.address.id, to_id: fromAddress.address.id }, this.DBConn ); if (reverseDelegation) throw new Error('Reverse Delegation already exists'); - // Cannot merge if both have progress. - if (!fromAddress.isNew && !toAddress.isNew) { - throw new Error('Both A and B have progress. Cannot merge.'); - } - // If "TO" has "TO" delegations, it is already owned by another wallet. // To reuse this address, cancel delegations first. const [toAddressHasTo] = await getDelegationsTo.run( @@ -157,21 +177,25 @@ export class DelegateWallet { } private async cmdDelegate(from: string, to: string): Promise { - const fromAddress = await this.getOrCreateNewAddress(from); - const toAddress = await this.getOrCreateNewAddress(to); + let fromAddress = await this.getAddress(from); + let toAddress = await this.getAddress(to); await this.validateDelegate(fromAddress, toAddress); - // Case 1: - // "from" is New, "to" has progress. - // swap IDs. - if (fromAddress.isNew && !toAddress.isNew) { - await this.swap(fromAddress.address.address, toAddress.address.address); - const [newToAddressId] = await getAddressFromAddress.run( - { address: toAddress.address.address }, - this.DBConn - ); - fromAddress.address.id = toAddress.address.id; - toAddress.address.id = newToAddressId.id; + if (!fromAddress.exists && toAddress.exists) { + // Case 1: + // "from" is New, "to" has progress. + // swap IDs. + const { newA, newB } = await this.swap(toAddress, fromAddress); + fromAddress = newA; + toAddress = newB; + } else { + // Case 2: + // Do not swap progress. + // Create new addresses + if (!fromAddress.exists) + fromAddress = { address: await this.createAddress(fromAddress.address), exists: true }; + if (!toAddress.exists) + toAddress = { address: await this.createAddress(toAddress.address), exists: true }; } // Case 2: @@ -193,17 +217,22 @@ export class DelegateWallet { this.DBConn ); + // TODO this is clears the entire cache. We can only clear necessary elements. addressCache.clear(); } // Migrate. // To gains From ID, and From is assigned a new ID. private async cmdMigrate(from: string, to: string): Promise { - const [fromAddress] = await getAddressFromAddress.run({ address: from }, this.DBConn); - if (!fromAddress) throw new Error('Invalid Address'); - - const toAddress = await this.getOrCreateNewAddress(to); - await this.swap(fromAddress.address, toAddress.address.address); + let fromAddress = await this.getAddress(from); + let toAddress = await this.getAddress(to); + await this.validateDelegate(fromAddress, toAddress); + if (!fromAddress.exists && toAddress.exists) { + await this.swap(toAddress, fromAddress); + } else { + throw new Error('Cannot migrate'); + } + // TODO this is clears the entire cache. We can only clear necessary elements. addressCache.clear(); } @@ -213,6 +242,8 @@ export class DelegateWallet { const [toAddress] = await getAddressFromAddress.run({ address: to }, this.DBConn); if (!toAddress) throw new Error('Invalid Address'); await deleteDelegationsFrom.run({ from_id: toAddress.id }, this.DBConn); + + // TODO this is clears the entire cache. We can only clear necessary elements. addressCache.clear(); } diff --git a/packages/engine/paima-sm/src/index.ts b/packages/engine/paima-sm/src/index.ts index 382ec3041..31879dcbf 100644 --- a/packages/engine/paima-sm/src/index.ts +++ b/packages/engine/paima-sm/src/index.ts @@ -343,8 +343,8 @@ async function processUserInputs( } else { // If wallet does not exist in address table: create it. if (inputData.userId === NO_USER_ID) { - const newAddress = await delegateWallet.getOrCreateNewAddress(inputData.userAddress); - inputData.userId = newAddress.address.id; + const newAddress = await delegateWallet.createAddress(inputData.userAddress); + inputData.userId = newAddress.id; } // Trigger STF const sqlQueries = await gameStateTransition( diff --git a/packages/node-sdk/paima-db/src/delegate-wallet.ts b/packages/node-sdk/paima-db/src/delegate-wallet.ts index a0daab904..da07e5e4e 100644 --- a/packages/node-sdk/paima-db/src/delegate-wallet.ts +++ b/packages/node-sdk/paima-db/src/delegate-wallet.ts @@ -4,10 +4,9 @@ import type { } from './sql/wallet-delegation.queries.js'; import { getAddressFromAddress, - getAddressFromId, getDelegationsFromWithAddress, - getDelegationsTo, getDelegationsToWithAddress, + getMainAddressFromAddress, } from './sql/wallet-delegation.queries.js'; import type { PoolClient } from 'pg'; @@ -32,28 +31,23 @@ export async function getMainAddress( if (addressMapping) return addressMapping; // get main address. - // const addressResult = await this.getOrCreateNewAddress(address); - const [addressResult] = await getAddressFromAddress.run({ address }, DBConn); + const [addressResult] = await getMainAddressFromAddress.run({ address }, DBConn); + if (!addressResult) { // This wallet has never been used before. // This value will get updated before sent to the STF. return { address, id: NO_USER_ID }; } - // if exists we have to check if it is a delegation. - const [delegate] = await getDelegationsTo.run({ to_id: addressResult.id }, DBConn); - if (!delegate) { - // is main address or has no delegations. - addressMapping = { address: addressResult.address, id: addressResult.id }; - addressCache.set(address, addressMapping); - return addressMapping; - } + const result = addressResult.from_address + ? // this wallet is a delegate. + { address: addressResult.from_address, id: addressResult.from_id } + : // this is the main wallet or does not have delegations. + { address: addressResult.to_address, id: addressResult.to_id }; + + addressCache.set(address, result); - // if is delegation, get main address. - const [mainAddress] = await getAddressFromId.run({ id: delegate.from_id }, DBConn); - addressMapping = { address: mainAddress.address, id: mainAddress.id }; - addressCache.set(address, addressMapping); - return addressMapping; + return result; } export async function getRelatedWallets( diff --git a/packages/node-sdk/paima-db/src/sql/wallet-delegation.queries.ts b/packages/node-sdk/paima-db/src/sql/wallet-delegation.queries.ts index 2c549b911..96133a2f8 100644 --- a/packages/node-sdk/paima-db/src/sql/wallet-delegation.queries.ts +++ b/packages/node-sdk/paima-db/src/sql/wallet-delegation.queries.ts @@ -482,3 +482,40 @@ const updateDelegateFromIR: any = {"usedParamSet":{"new_from":true,"old_from":tr export const updateDelegateFrom = new PreparedQuery(updateDelegateFromIR); +/** 'GetMainAddressFromAddress' parameters type */ +export interface IGetMainAddressFromAddressParams { + address: string; +} + +/** 'GetMainAddressFromAddress' return type */ +export interface IGetMainAddressFromAddressResult { + from_address: string; + from_id: number; + to_address: string; + to_id: number; +} + +/** 'GetMainAddressFromAddress' query type */ +export interface IGetMainAddressFromAddressQuery { + params: IGetMainAddressFromAddressParams; + result: IGetMainAddressFromAddressResult; +} + +const getMainAddressFromAddressIR: any = {"usedParamSet":{"address":true},"params":[{"name":"address","required":true,"transform":{"type":"scalar"},"locs":[{"a":332,"b":340}]}],"statement":"select addr.id as to_id, \n addr.address as to_address,\n main_addr.id as from_id, \n main_addr.address as from_address\nfrom addresses addr\nleft join delegations on delegations.to_id = addr.id\nleft join addresses main_addr on delegations.from_id = main_addr.id\nwhere addr.address = :address!"}; + +/** + * Query generated from SQL: + * ``` + * select addr.id as to_id, + * addr.address as to_address, + * main_addr.id as from_id, + * main_addr.address as from_address + * from addresses addr + * left join delegations on delegations.to_id = addr.id + * left join addresses main_addr on delegations.from_id = main_addr.id + * where addr.address = :address! + * ``` + */ +export const getMainAddressFromAddress = new PreparedQuery(getMainAddressFromAddressIR); + + diff --git a/packages/node-sdk/paima-db/src/sql/wallet-delegation.sql b/packages/node-sdk/paima-db/src/sql/wallet-delegation.sql index 6cfb7726e..45ef15c4e 100644 --- a/packages/node-sdk/paima-db/src/sql/wallet-delegation.sql +++ b/packages/node-sdk/paima-db/src/sql/wallet-delegation.sql @@ -72,3 +72,14 @@ UPDATE delegations SET from_id = :new_from! WHERE from_id = :old_from! AND to_id = :old_to!; + +/* @name getMainAddressFromAddress */ +select addr.id as to_id, + addr.address as to_address, + main_addr.id as from_id, + main_addr.address as from_address +from addresses addr +left join delegations on delegations.to_id = addr.id +left join addresses main_addr on delegations.from_id = main_addr.id +where addr.address = :address! +; diff --git a/packages/paima-sdk/paima-mw-core/src/wallet-connect/index.ts b/packages/paima-sdk/paima-mw-core/src/wallet-connect/index.ts index 9dc155b59..400e43590 100644 --- a/packages/paima-sdk/paima-mw-core/src/wallet-connect/index.ts +++ b/packages/paima-sdk/paima-mw-core/src/wallet-connect/index.ts @@ -16,8 +16,9 @@ import { PolkadotConnector, AlgorandConnector, } from '@paima/providers'; -import { ENV } from '@paima/utils'; +import { AddressType, ENV } from '@paima/utils'; import { paimaEndpoints } from '../index.js'; +import assertNever from 'assert-never'; export async function walletConnect( from: string, @@ -99,8 +100,6 @@ export async function walletConnectCancelDelegations( } } -export type WalletConnectHelperTypes = 'EVM' | 'Cardano' | 'Polkadot' | 'Algorand'; - export class WalletConnectHelper { private static readonly DELEGATE_WALLET_PREFIX = 'DELEGATE-WALLET'; private static readonly SEP = ':'; @@ -111,18 +110,15 @@ export class WalletConnectHelper { }${subMessage.toLocaleLowerCase()}${WalletConnectHelper.SEP}${ENV.CONTRACT_ADDRESS}`; } - private async signWithExternalWallet( - walletType: WalletConnectHelperTypes, - message: string - ): Promise { + private async signWithExternalWallet(walletType: AddressType, message: string): Promise { switch (walletType) { - case 'EVM': + case AddressType.EVM: return (await EvmInjectedConnector.instance().getProvider()?.signMessage(message)) || ''; - case 'Cardano': + case AddressType.CARDANO: return (await CardanoConnector.instance().getProvider()?.signMessage(message)) || ''; - case 'Polkadot': + case AddressType.POLKADOT: return (await PolkadotConnector.instance().getProvider()?.signMessage(message)) || ''; - case 'Algorand': + case AddressType.ALGORAND: return (await AlgorandConnector.instance().getProvider()?.signMessage(message)) || ''; default: throw new Error('Invalid wallet type'); @@ -130,7 +126,7 @@ export class WalletConnectHelper { } public async connectExternalWalletAndSign( - walletType: WalletConnectHelperTypes, + walletType: AddressType, walletName: string, subMessage: string ): Promise<{ @@ -140,7 +136,7 @@ export class WalletConnectHelper { }> { let loginInfo: LoginInfo; switch (walletType) { - case 'EVM': + case AddressType.EVM: loginInfo = { mode: WalletMode.EvmInjected, preferBatchedMode: false, @@ -150,7 +146,7 @@ export class WalletConnectHelper { checkChainId: false, }; break; - case 'Cardano': + case AddressType.CARDANO: loginInfo = { mode: WalletMode.Cardano, preference: { @@ -158,14 +154,16 @@ export class WalletConnectHelper { }, }; break; - case 'Polkadot': + case AddressType.POLKADOT: loginInfo = { mode: WalletMode.Polkadot }; break; - case 'Algorand': + case AddressType.ALGORAND: loginInfo = { mode: WalletMode.Algorand }; break; + case AddressType.UNKNOWN: + throw new Error('AddressTypes cannot be Unknown.'); default: - throw new Error('No supported wallet type.'); + assertNever(walletType); } const response = await paimaEndpoints.userWalletLogin(loginInfo, false);