Skip to content

Commit

Permalink
Swap with no delete. Unified Enums
Browse files Browse the repository at this point in the history
  • Loading branch information
acedward committed Dec 18, 2023
1 parent 1471d11 commit 60c124f
Show file tree
Hide file tree
Showing 6 changed files with 147 additions and 76 deletions.
111 changes: 71 additions & 40 deletions packages/engine/paima-sm/src/delegate-wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { CryptoManager } from '@paima/crypto';
import type { IGetAddressFromAddressResult } from '@paima/db';
import {
addressCache,
deleteAddress,
deleteDelegationsFrom,
getAddressFromAddress,
getAddressFromId,
Expand All @@ -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';
Expand Down Expand Up @@ -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<IGetAddressFromAddressResult> {
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<AddressWrapper> {
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<void> {
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<void> {
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(
Expand All @@ -157,21 +177,25 @@ export class DelegateWallet {
}

private async cmdDelegate(from: string, to: string): Promise<void> {
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:
Expand All @@ -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<void> {
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();
}

Expand All @@ -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();
}

Expand Down
4 changes: 2 additions & 2 deletions packages/engine/paima-sm/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
28 changes: 11 additions & 17 deletions packages/node-sdk/paima-db/src/delegate-wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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(
Expand Down
37 changes: 37 additions & 0 deletions packages/node-sdk/paima-db/src/sql/wallet-delegation.queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -482,3 +482,40 @@ const updateDelegateFromIR: any = {"usedParamSet":{"new_from":true,"old_from":tr
export const updateDelegateFrom = new PreparedQuery<IUpdateDelegateFromParams,IUpdateDelegateFromResult>(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<IGetMainAddressFromAddressParams,IGetMainAddressFromAddressResult>(getMainAddressFromAddressIR);


11 changes: 11 additions & 0 deletions packages/node-sdk/paima-db/src/sql/wallet-delegation.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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!
;
32 changes: 15 additions & 17 deletions packages/paima-sdk/paima-mw-core/src/wallet-connect/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 = ':';
Expand All @@ -111,26 +110,23 @@ export class WalletConnectHelper {
}${subMessage.toLocaleLowerCase()}${WalletConnectHelper.SEP}${ENV.CONTRACT_ADDRESS}`;
}

private async signWithExternalWallet(
walletType: WalletConnectHelperTypes,
message: string
): Promise<string> {
private async signWithExternalWallet(walletType: AddressType, message: string): Promise<string> {
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');
}
}

public async connectExternalWalletAndSign(
walletType: WalletConnectHelperTypes,
walletType: AddressType,
walletName: string,
subMessage: string
): Promise<{
Expand All @@ -140,7 +136,7 @@ export class WalletConnectHelper {
}> {
let loginInfo: LoginInfo;
switch (walletType) {
case 'EVM':
case AddressType.EVM:
loginInfo = {
mode: WalletMode.EvmInjected,
preferBatchedMode: false,
Expand All @@ -150,22 +146,24 @@ export class WalletConnectHelper {
checkChainId: false,
};
break;
case 'Cardano':
case AddressType.CARDANO:
loginInfo = {
mode: WalletMode.Cardano,
preference: {
name: walletName,
},
};
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);
Expand Down

0 comments on commit 60c124f

Please sign in to comment.