From 68607804efc1191ff396e05f20466526246b7d87 Mon Sep 17 00:00:00 2001 From: N V Rakesh Reddy Date: Thu, 7 Aug 2025 15:34:05 +0530 Subject: [PATCH] feat(sdk-coin-polyx): add token builders TICKET: WIN-6578 --- modules/abstract-substrate/src/lib/iface.ts | 51 ++--- .../src/lib/nativeTransferBuilder.ts | 9 +- modules/sdk-coin-polyx/src/lib/iface.ts | 82 +++++++- modules/sdk-coin-polyx/src/lib/index.ts | 2 + .../src/lib/preApproveAssetBuilder.ts | 90 +++++++++ .../src/lib/registerDidWithCDDBuilder.ts | 8 +- .../src/lib/tokenTransferBuilder.ts | 175 ++++++++++++++++++ modules/sdk-coin-polyx/src/lib/transaction.ts | 62 ++++++- .../src/lib/transactionBuilderFactory.ts | 18 +- .../sdk-coin-polyx/src/lib/transferBuilder.ts | 6 +- modules/sdk-coin-polyx/src/lib/txnSchema.ts | 47 +++++ .../sdk-coin-polyx/test/resources/index.ts | 7 + modules/sdk-coin-polyx/test/unit/polyx.ts | 2 +- .../preApproveAssetBuilder.ts | 122 ++++++++++++ .../tokenTransferBuilder.ts | 139 ++++++++++++++ modules/sdk-coin-tao/test/unit/tao.ts | 2 +- 16 files changed, 780 insertions(+), 42 deletions(-) create mode 100644 modules/sdk-coin-polyx/src/lib/preApproveAssetBuilder.ts create mode 100644 modules/sdk-coin-polyx/src/lib/tokenTransferBuilder.ts create mode 100644 modules/sdk-coin-polyx/test/unit/transactionBuilder/preApproveAssetBuilder.ts create mode 100644 modules/sdk-coin-polyx/test/unit/transactionBuilder/tokenTransferBuilder.ts diff --git a/modules/abstract-substrate/src/lib/iface.ts b/modules/abstract-substrate/src/lib/iface.ts index 4e4eab081a..4a578a0765 100644 --- a/modules/abstract-substrate/src/lib/iface.ts +++ b/modules/abstract-substrate/src/lib/iface.ts @@ -17,70 +17,74 @@ export enum SectionNames { /** * Method names for the transaction method. Names change based on the type of transaction e.g 'bond' for the staking transaction + * + * This is implemented as a const object with string literals to allow for extension in derived modules. */ -export enum MethodNames { +export const MethodNames = { /** * Transfer the entire transferable balance from the caller account. * * @see https://polkadot.js.org/docs/substrate/extrinsics/#transferalldest-multiaddress-keep_alive-bool */ - TransferAll = 'transferAll', + TransferAll: 'transferAll' as const, /** * Same as the transfer call, but with a check that the transfer will not kill the origin account. * * @see https://polkadot.js.org/docs/substrate/extrinsics/#transferkeepalivedest-multiaddress-value-compactu128 */ - TransferKeepAlive = 'transferKeepAlive', + TransferKeepAlive: 'transferKeepAlive' as const, /** * Transfer funds with an optional memo attached. * The memo allows adding context or metadata to the transaction, commonly used for recordkeeping or identification. * * @see https://developers.polymesh.network/sdk-docs/enums/Generated/Types/BalancesTx/#transferwithmemo */ - TransferWithMemo = 'transferWithMemo', - AddStake = 'addStake', - RemoveStake = 'removeStake', - + TransferWithMemo: 'transferWithMemo' as const, + AddStake: 'addStake' as const, + RemoveStake: 'removeStake' as const, /** * Take the origin account as a stash and lock up value of its balance. */ - Bond = 'bond', + Bond: 'bond' as const, /** * Add some extra amount that have appeared in the stash free_balance into the balance up for staking. */ - BondExtra = 'bondExtra', + BondExtra: 'bondExtra' as const, /** * Declare the desire to nominate targets for the origin controller. */ - Nominate = 'nominate', + Nominate: 'nominate' as const, /** * Declare no desire to either validate or nominate. */ - Chill = 'chill', + Chill: 'chill' as const, /** * Schedule a portion of the stash to be unlocked ready for transfer out after the bond period ends. */ - Unbond = 'unbond', + Unbond: 'unbond' as const, /** * Remove any unlocked chunks from the unlocking queue from our management. */ - WithdrawUnbonded = 'withdrawUnbonded', + WithdrawUnbonded: 'withdrawUnbonded' as const, /** * Send a batch of dispatch calls. */ - Batch = 'batch', + Batch: 'batch' as const, /** * Send a batch of dispatch calls and atomically execute them. */ - BatchAll = 'batchAll', + BatchAll: 'batchAll' as const, +} as const; - /** - * Registers a Decentralized Identifier (DID) along with Customer Due Diligence (CDD) information. - * - * @see https://developers.polymesh.network/sdk-docs/enums/Generated/Types/IdentityTx/#cddregisterdidwithcdd - */ - RegisterDidWithCDD = 'cddRegisterDidWithCdd', -} +/** + * Type representing the keys of the MethodNames object + */ +export type MethodNamesType = keyof typeof MethodNames; + +/** + * Type representing the values of the MethodNames object + */ +export type MethodNamesValues = (typeof MethodNames)[MethodNamesType]; /** * The transaction data returned from the toJson() function of a transaction @@ -106,6 +110,7 @@ export interface TxData { netuid?: string; numSlashingSpans?: number; batchCalls?: BatchCallObject[]; + memo?: string; } /** @@ -193,7 +198,7 @@ export interface TxMethod { | UnbondArgs | WithdrawUnbondedArgs | BatchArgs; - name: MethodNames; + name: MethodNamesValues; pallet: string; } diff --git a/modules/abstract-substrate/src/lib/nativeTransferBuilder.ts b/modules/abstract-substrate/src/lib/nativeTransferBuilder.ts index 717f98c92d..efc990d63f 100644 --- a/modules/abstract-substrate/src/lib/nativeTransferBuilder.ts +++ b/modules/abstract-substrate/src/lib/nativeTransferBuilder.ts @@ -121,13 +121,16 @@ export abstract class NativeTransferBuilder extends TransactionBuilder { /** @inheritdoc */ protected fromImplementation(rawTransaction: string): Transaction { const tx = super.fromImplementation(rawTransaction); - if (this._method?.name === MethodNames.TransferKeepAlive) { + if (!this._method || !this._method.args) { + throw new InvalidTransactionError('Transaction method or args are undefined'); + } + if (this._method.name === MethodNames.TransferKeepAlive) { const txMethod = this._method.args as TransferArgs; this.amount(txMethod.value); this.to({ address: utils.decodeSubstrateAddress(txMethod.dest.id, this.getAddressFormat()), }); - } else if (this._method?.name === MethodNames.TransferAll) { + } else if (this._method.name === MethodNames.TransferAll) { this._sweepFreeBalance = true; const txMethod = this._method.args as TransferAllArgs; this.sweep(txMethod.keepAlive); @@ -136,7 +139,7 @@ export abstract class NativeTransferBuilder extends TransactionBuilder { }); } else { throw new InvalidTransactionError( - `Invalid Transaction Type: ${this._method?.name}. Expected a transferKeepAlive or a proxy transferKeepAlive transaction` + `Invalid Transaction Type: ${this._method.name}. Expected a transferKeepAlive or a proxy transferKeepAlive transaction` ); } return tx; diff --git a/modules/sdk-coin-polyx/src/lib/iface.ts b/modules/sdk-coin-polyx/src/lib/iface.ts index 0c31165440..5b007db8fe 100644 --- a/modules/sdk-coin-polyx/src/lib/iface.ts +++ b/modules/sdk-coin-polyx/src/lib/iface.ts @@ -4,13 +4,88 @@ import { DecodedUnsignedTx } from '@substrate/txwrapper-core/lib/types'; export type AnyJson = string | number | boolean | null | { [key: string]: AnyJson } | Array; +/** + * Extended TxData interface for Polyx transactions + * Adds assetId field to the base TxData interface from abstract-substrate + */ +export interface TxData extends Interface.TxData { + assetId?: string; + fromDID?: string; + toDID?: string; +} + +/** + * Settlement type for Polyx transactions + */ +export enum SettlementType { + SettleOnAffirmation = 'SettleOnAffirmation', +} + +/** + * Portfolio kind for Polyx transactions + */ +export enum PortfolioKind { + Default = 'Default', +} + +/** + * Method names for Polyx transactions. + * Extends the base MethodNames from Interface with additional Polyx-specific methods. + */ +export const MethodNames = { + // Include all values from the base object + ...Interface.MethodNames, + + /** + * Registers a Decentralized Identifier (DID) along with Customer Due Diligence (CDD) information. + * + * @see https://developers.polymesh.network/sdk-docs/enums/Generated/Types/IdentityTx/#cddregisterdidwithcdd + */ + RegisterDidWithCDD: 'cddRegisterDidWithCdd' as const, + + /** + * Pre-approves an asset. + */ + PreApproveAsset: 'preApproveAsset' as const, + + AddAndAffirmWithMediators: 'addAndAffirmWithMediators' as const, +} as const; + +// Create a type that represents the keys of this object +export type MethodNamesType = keyof typeof MethodNames; + +// Create a type that represents the values of this object +export type MethodNamesValues = (typeof MethodNames)[MethodNamesType]; + export interface RegisterDidWithCDDArgs extends Args { targetAccount: string; secondaryKeys: []; expiry: null; } -export interface TxMethod extends Omit { +export interface PreApproveAssetArgs extends Args { + assetId: string; +} + +export interface AddAndAffirmWithMediatorsArgs extends Args { + venueId: null; + settlementType: SettlementType.SettleOnAffirmation; + tradeDate: null; + valueDate: null; + legs: Array<{ + fungible: { + sender: { did: string; kind: PortfolioKind.Default }; + receiver: { did: string; kind: PortfolioKind.Default }; + assetId: string; + amount: string; + }; + }>; + portfolios: Array<{ did: string; kind: PortfolioKind.Default }>; + instructionMemo: string; + mediators: []; +} + +export interface TxMethod extends Omit { args: | Interface.TransferArgs | Interface.TransferAllArgs @@ -23,7 +98,10 @@ export interface TxMethod extends Omit { | Interface.UnbondArgs | Interface.WithdrawUnbondedArgs | Interface.BatchArgs - | RegisterDidWithCDDArgs; + | RegisterDidWithCDDArgs + | PreApproveAssetArgs + | AddAndAffirmWithMediatorsArgs; + name: MethodNamesValues; } export interface DecodedTx extends Omit { diff --git a/modules/sdk-coin-polyx/src/lib/index.ts b/modules/sdk-coin-polyx/src/lib/index.ts index 3cd45f4114..e33bdeec8f 100644 --- a/modules/sdk-coin-polyx/src/lib/index.ts +++ b/modules/sdk-coin-polyx/src/lib/index.ts @@ -12,6 +12,8 @@ export { TransactionBuilderFactory } from './transactionBuilderFactory'; export { PolyxBaseBuilder } from './baseBuilder'; export { TransferBuilder } from './transferBuilder'; export { RegisterDidWithCDDBuilder } from './registerDidWithCDDBuilder'; +export { PreApproveAssetBuilder } from './preApproveAssetBuilder'; +export { TokenTransferBuilder } from './tokenTransferBuilder'; export { Transaction as PolyxTransaction } from './transaction'; export { BondExtraBuilder } from './bondExtraBuilder'; export { BatchStakingBuilder as BatchBuilder } from './batchStakingBuilder'; diff --git a/modules/sdk-coin-polyx/src/lib/preApproveAssetBuilder.ts b/modules/sdk-coin-polyx/src/lib/preApproveAssetBuilder.ts new file mode 100644 index 0000000000..0a8fbbec21 --- /dev/null +++ b/modules/sdk-coin-polyx/src/lib/preApproveAssetBuilder.ts @@ -0,0 +1,90 @@ +import { TransactionType, InvalidTransactionError } from '@bitgo/sdk-core'; +import { BaseCoin as CoinConfig } from '@bitgo/statics'; +import { DecodedSignedTx, DecodedSigningPayload, defineMethod, UnsignedTransaction } from '@substrate/txwrapper-core'; +import { Interface } from '@bitgo/abstract-substrate'; +import { PolyxBaseBuilder } from './baseBuilder'; +import { TxMethod, PreApproveAssetArgs, MethodNames } from './iface'; +import { PreApproveAssetTransactionSchema } from './txnSchema'; +import { Transaction } from './transaction'; + +export class PreApproveAssetBuilder extends PolyxBaseBuilder { + protected _assetId: string; + protected _method: TxMethod; + + constructor(_coinConfig: Readonly) { + super(_coinConfig); + this._transaction = new Transaction(_coinConfig); + } + + protected get transactionType(): TransactionType { + return TransactionType.TrustLine; + } + + protected buildTransaction(): UnsignedTransaction { + const baseTxInfo = this.createBaseTxInfo(); + return this.preApproveAsset( + { + assetId: this._assetId, + }, + baseTxInfo + ); + } + + /** + * Sets the asset ID for the pre-approval transaction. + * + * @param {string} assetId - The ID of the asset to be pre-approved. + * @returns {this} The current instance of the builder. + */ + assetId(assetId: string): this { + this._assetId = assetId; + return this; + } + + /** @inheritdoc */ + protected fromImplementation(rawTransaction: string): Transaction { + const tx = super.fromImplementation(rawTransaction); + if (this._method?.name === MethodNames.PreApproveAsset) { + const txMethod = this._method.args as PreApproveAssetArgs; + this.assetId(txMethod.assetId); + } else { + throw new InvalidTransactionError(`Invalid Transaction Type: ${this._method?.name}. Expected preApproveAsset`); + } + return tx; + } + + /** @inheritdoc */ + validateDecodedTransaction(decodedTxn: DecodedSigningPayload | DecodedSignedTx, rawTransaction?: string): void { + if (decodedTxn.method?.name === MethodNames.PreApproveAsset) { + const txMethod = decodedTxn.method.args as PreApproveAssetArgs; + const assetId = txMethod.assetId; + + const validationResult = PreApproveAssetTransactionSchema.validate({ assetId }); + if (!validationResult) { + throw new InvalidTransactionError('Invalid transaction: assetId is required'); + } + } + } + + /** + * Construct a transaction to pre-approve an asset + * + * @param {PreApproveAssetArgs} args Arguments to be passed to the preApproveAsset method + * @param {Interface.CreateBaseTxInfo} info Base txn info required to construct the pre-approve asset txn + * @returns {UnsignedTransaction} an unsigned transaction for asset pre-approval + */ + private preApproveAsset(args: PreApproveAssetArgs, info: Interface.CreateBaseTxInfo): UnsignedTransaction { + console.log(`PreApproveAssetBuilder: preApproveAsset called with args: ${JSON.stringify(args)}`); + return defineMethod( + { + method: { + args, + name: 'preApproveAsset', + pallet: 'asset', + }, + ...info.baseTxInfo, + }, + info.options + ); + } +} diff --git a/modules/sdk-coin-polyx/src/lib/registerDidWithCDDBuilder.ts b/modules/sdk-coin-polyx/src/lib/registerDidWithCDDBuilder.ts index ceb07d5635..6ae4a409ea 100644 --- a/modules/sdk-coin-polyx/src/lib/registerDidWithCDDBuilder.ts +++ b/modules/sdk-coin-polyx/src/lib/registerDidWithCDDBuilder.ts @@ -3,7 +3,7 @@ import { PolyxBaseBuilder } from './baseBuilder'; import { DecodedSignedTx, DecodedSigningPayload, defineMethod, UnsignedTransaction } from '@substrate/txwrapper-core'; import { BaseCoin as CoinConfig } from '@bitgo/statics'; import { TransactionType, BaseAddress, InvalidTransactionError } from '@bitgo/sdk-core'; -import { RegisterDidWithCDDArgs, TxMethod } from './iface'; +import { RegisterDidWithCDDArgs, TxMethod, MethodNames } from './iface'; import { RegisterDidWithCDDTransactionSchema } from './txnSchema'; import { Transaction } from './transaction'; @@ -48,18 +48,18 @@ export class RegisterDidWithCDDBuilder extends PolyxBaseBuilder { + protected _assetId: string; + protected _amount: string; + protected _memo: string; + protected _fromDID: string; + protected _toDID: string; + + constructor(_coinConfig: Readonly) { + super(_coinConfig); + this._transaction = new Transaction(_coinConfig); + } + + protected get transactionType(): TransactionType { + return TransactionType.SendToken; + } + + protected buildTransaction(): UnsignedTransaction { + const baseTxInfo = this.createBaseTxInfo(); + return this.addAndAffirmWithMediators( + { + venueId: null, + settlementType: SettlementType.SettleOnAffirmation, + tradeDate: null, + valueDate: null, + legs: [ + { + fungible: { + sender: { + did: this._fromDID, + kind: PortfolioKind.Default, + }, + receiver: { + did: this._toDID, + kind: PortfolioKind.Default, + }, + assetId: this._assetId, + amount: this._amount, + }, + }, + ], + portfolios: [ + { + did: this._fromDID, + kind: PortfolioKind.Default, + }, + ], + instructionMemo: this._memo, + mediators: [], + }, + baseTxInfo + ); + } + + /** + * Sets the amount to transfer. + * + * @param {string} amount - The amount to transfer. + * @returns {this} The current instance of the builder. + */ + assetId(assetId: string): this { + this._assetId = assetId; + return this; + } + + /** + * Sets the amount to transfer. + * + * @param {string} amount - The amount to transfer. + * @returns {this} The current instance of the builder. + */ + amount(amount: string): this { + this._amount = amount; + return this; + } + + /** + * Sets the memo for the transaction. + * Pads the memo on the left with zeros to ensure it is 32 characters long. + * + * @param {string} memo - The memo for the transaction. + * @returns {this} The current instance of the builder. + */ + memo(memo: string): this { + const paddedMemo = memo.padStart(32, '0'); + this._memo = paddedMemo; + return this; + } + + /** + * Sets the sender DID. + * + * @param {string} fromDID - The sender DID. + * @returns {this} The current instance of the builder. + */ + fromDID(fromDID: string): this { + this._fromDID = fromDID; + return this; + } + + /** + * Sets the receiver DID. + * + * @param {string} toDID - The receiver DID. + * @returns {this} The current instance of the builder. + */ + toDID(toDID: string): this { + this._toDID = toDID; + return this; + } + + /** @inheritdoc */ + protected fromImplementation(rawTransaction: string): Transaction { + const tx = super.fromImplementation(rawTransaction); + if (this._method?.name === MethodNames.AddAndAffirmWithMediators) { + const txMethod = this._method.args as AddAndAffirmWithMediatorsArgs; + this.assetId(txMethod.legs[0].fungible.assetId); + this.amount(txMethod.legs[0].fungible.amount); + this.memo(txMethod.instructionMemo); + this.fromDID(txMethod.legs[0].fungible.sender.did); + this.toDID(txMethod.legs[0].fungible.receiver.did); + } else { + throw new Error(`Invalid Transaction Type: ${this._method?.name}. Expected AddAndAffirmWithMediators`); + } + return tx; + } + + /** @inheritdoc */ + validateDecodedTransaction(decodedTxn: DecodedSigningPayload | DecodedSignedTx, rawTransaction?: string): void { + if (decodedTxn.method?.name === MethodNames.AddAndAffirmWithMediators) { + const txMethod = decodedTxn.method.args as AddAndAffirmWithMediatorsArgs; + const assetId = txMethod.legs[0].fungible.assetId; + const amount = txMethod.legs[0].fungible.amount; + const memo = txMethod.instructionMemo; + const fromDID = txMethod.legs[0].fungible.sender.did; + const toDID = txMethod.legs[0].fungible.receiver.did; + + const validationResult = AddAndAffirmWithMediatorsTransactionSchema.validate({ + assetId, + amount, + memo, + fromDID, + toDID, + }); + if (validationResult.error) { + throw new Error(`Invalid transaction: ${validationResult.error.message}`); + } + } + } + + private addAndAffirmWithMediators( + args: AddAndAffirmWithMediatorsArgs, + info: Interface.CreateBaseTxInfo + ): UnsignedTransaction { + return defineMethod( + { + method: { + args, + name: 'addAndAffirmWithMediators', + pallet: 'settlement', + }, + ...info.baseTxInfo, + }, + info.options + ); + } +} diff --git a/modules/sdk-coin-polyx/src/lib/transaction.ts b/modules/sdk-coin-polyx/src/lib/transaction.ts index d6b9b27f25..9a0b7570dc 100644 --- a/modules/sdk-coin-polyx/src/lib/transaction.ts +++ b/modules/sdk-coin-polyx/src/lib/transaction.ts @@ -1,8 +1,8 @@ -import { Transaction as SubstrateTransaction, Interface, utils, KeyPair } from '@bitgo/abstract-substrate'; +import { Transaction as SubstrateTransaction, utils, KeyPair } from '@bitgo/abstract-substrate'; import { InvalidTransactionError, TransactionType } from '@bitgo/sdk-core'; import { construct, decode } from '@substrate/txwrapper-polkadot'; import { decodeAddress } from '@polkadot/keyring'; -import { DecodedTx, RegisterDidWithCDDArgs } from './iface'; +import { DecodedTx, RegisterDidWithCDDArgs, PreApproveAssetArgs, TxData, AddAndAffirmWithMediatorsArgs } from './iface'; import polyxUtils from './utils'; export class Transaction extends SubstrateTransaction { @@ -18,7 +18,7 @@ export class Transaction extends SubstrateTransaction { } /** @inheritdoc */ - toJson(): Interface.TxData { + toJson(): TxData { if (!this._substrateTransaction) { throw new InvalidTransactionError('Empty transaction'); } @@ -29,7 +29,7 @@ export class Transaction extends SubstrateTransaction { isImmortalEra: utils.isZeroHex(this._substrateTransaction.era), }) as unknown as DecodedTx; - const result: Interface.TxData = { + const result: TxData = { id: construct.txHash(this.toBroadcastFormat()), sender: decodedTx.address, referenceBlock: decodedTx.blockHash, @@ -51,8 +51,20 @@ export class Transaction extends SubstrateTransaction { }); result.to = keypairDest.getAddress(this.getAddressFormat()); result.amount = '0'; // RegisterDidWithCDD does not transfer any value + } else if (this.type === TransactionType.TrustLine) { + const { assetId } = txMethod as PreApproveAssetArgs; + result.assetId = assetId; + result.sender = decodedTx.address; + result.amount = '0'; // Pre-approval does not transfer any value + } else if (this.type === TransactionType.SendToken) { + const sendTokenArgs = txMethod as AddAndAffirmWithMediatorsArgs; + result.fromDID = sendTokenArgs.legs[0].fungible.sender.did; + result.toDID = sendTokenArgs.legs[0].fungible.receiver.did; + result.amount = sendTokenArgs.legs[0].fungible.amount.toString(); + result.assetId = sendTokenArgs.legs[0].fungible.assetId; + result.memo = sendTokenArgs.instructionMemo; } else { - return super.toJson(); + return super.toJson() as TxData; } return result; @@ -72,6 +84,10 @@ export class Transaction extends SubstrateTransaction { if (this.type === TransactionType.WalletInitialization) { this.decodeInputsAndOutputsForRegisterDidWithCDD(decodedTx); + } else if (this.type === TransactionType.TrustLine) { + this.decodeInputsAndOutputsForPreApproveAsset(decodedTx); + } else if (this.type === TransactionType.SendToken) { + this.decodeInputsAndOutputsForSendToken(decodedTx); } } @@ -96,4 +112,40 @@ export class Transaction extends SubstrateTransaction { coin: this._coinConfig.name, }); } + + private decodeInputsAndOutputsForPreApproveAsset(decodedTx: DecodedTx) { + const sender = decodedTx.address; + const value = '0'; // Pre-approval does not transfer any value + + this._inputs.push({ + address: sender, + value, + coin: this._coinConfig.name, + }); + + this._outputs.push({ + address: sender, // In pre-approval, the output is the same as the input + value, + coin: this._coinConfig.name, + }); + } + + private decodeInputsAndOutputsForSendToken(decodedTx: DecodedTx) { + const txMethod = decodedTx.method.args as AddAndAffirmWithMediatorsArgs; + const fromDID = txMethod.legs[0].fungible.sender.did; + const toDID = txMethod.legs[0].fungible.receiver.did; + const amount = txMethod.legs[0].fungible.amount.toString(); + + this._inputs.push({ + address: fromDID, + value: amount, + coin: this._coinConfig.name, + }); + + this._outputs.push({ + address: toDID, + value: amount, + coin: this._coinConfig.name, + }); + } } diff --git a/modules/sdk-coin-polyx/src/lib/transactionBuilderFactory.ts b/modules/sdk-coin-polyx/src/lib/transactionBuilderFactory.ts index 0054c2976e..172b298e81 100644 --- a/modules/sdk-coin-polyx/src/lib/transactionBuilderFactory.ts +++ b/modules/sdk-coin-polyx/src/lib/transactionBuilderFactory.ts @@ -10,9 +10,11 @@ import { UnbondBuilder } from './unbondBuilder'; import { WithdrawUnbondedBuilder } from './withdrawUnbondedBuilder'; import utils from './utils'; import { Interface, SingletonRegistry, TransactionBuilder } from './'; -import { TxMethod, BatchCallObject } from './iface'; +import { TxMethod, BatchCallObject, MethodNames } from './iface'; import { Transaction as BaseTransaction } from '@bitgo/abstract-substrate'; import { Transaction as PolyxTransaction } from './transaction'; +import { PreApproveAssetBuilder } from './preApproveAssetBuilder'; +import { TokenTransferBuilder } from './tokenTransferBuilder'; export type SupportedTransaction = BaseTransaction | PolyxTransaction; @@ -32,6 +34,14 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory { return new RegisterDidWithCDDBuilder(this._coinConfig).material(this._material); } + getPreApproveAssetBuilder(): PreApproveAssetBuilder { + return new PreApproveAssetBuilder(this._coinConfig).material(this._material); + } + + getTokenTransferBuilder(): TokenTransferBuilder { + return new TokenTransferBuilder(this._coinConfig).material(this._material); + } + getBondExtraBuilder(): BondExtraBuilder { return new BondExtraBuilder(this._coinConfig).material(this._material); } @@ -77,8 +87,12 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory { const methodName = decodedTxn.method?.name; if (methodName === Interface.MethodNames.TransferWithMemo) { return this.getTransferBuilder(); - } else if (methodName === Interface.MethodNames.RegisterDidWithCDD) { + } else if (methodName === MethodNames.RegisterDidWithCDD) { return this.getRegisterDidWithCDDBuilder(); + } else if (methodName === MethodNames.PreApproveAsset) { + return this.getPreApproveAssetBuilder(); + } else if (methodName === MethodNames.AddAndAffirmWithMediators) { + return this.getTokenTransferBuilder(); } else if (methodName === 'bondExtra') { return this.getBondExtraBuilder(); } else if (methodName === 'batchAll') { diff --git a/modules/sdk-coin-polyx/src/lib/transferBuilder.ts b/modules/sdk-coin-polyx/src/lib/transferBuilder.ts index 0f077cb33a..2fe9d5c3ca 100644 --- a/modules/sdk-coin-polyx/src/lib/transferBuilder.ts +++ b/modules/sdk-coin-polyx/src/lib/transferBuilder.ts @@ -98,6 +98,10 @@ export class TransferBuilder extends PolyxBaseBuilder { /** @inheritdoc */ protected fromImplementation(rawTransaction: string): Transaction { const tx = super.fromImplementation(rawTransaction); + if (!this._method || !this._method.args) { + throw new InvalidTransactionError('Transaction method or args are undefined'); + } + if (this._method?.name === Interface.MethodNames.TransferWithMemo) { const txMethod = this._method.args as Interface.TransferWithMemoArgs; this.amount(txMethod.value); @@ -106,7 +110,7 @@ export class TransferBuilder extends PolyxBaseBuilder { }); this.memo(txMethod.memo); } else { - throw new InvalidTransactionError(`Invalid Transaction Type: ${this._method?.name}. Expected transferWithMemo`); + throw new InvalidTransactionError(`Invalid Transaction Type: ${this._method.name}. Expected transferWithMemo`); } return tx; } diff --git a/modules/sdk-coin-polyx/src/lib/txnSchema.ts b/modules/sdk-coin-polyx/src/lib/txnSchema.ts index c940fa43d4..1c96cdb442 100644 --- a/modules/sdk-coin-polyx/src/lib/txnSchema.ts +++ b/modules/sdk-coin-polyx/src/lib/txnSchema.ts @@ -30,6 +30,53 @@ export const RegisterDidWithCDDTransactionSchema = joi.object({ expiry: joi.valid(null).required(), }); +export const PreApproveAssetTransactionSchema = joi.object({ + assetId: joi.string().required(), +}); + +export const AddAndAffirmWithMediatorsTransactionSchema = joi.object({ + venueId: joi.valid(null).required(), + settlementType: joi.string().valid('SettleOnAffirmation').required(), + tradeDate: joi.valid(null).required(), + valueDate: joi.valid(null).required(), + legs: joi + .array() + .items( + joi.object({ + Fungible: joi + .object({ + sender: joi + .object({ + did: addressSchema.required(), + kind: joi.string().valid('Default').required(), + }) + .required(), + receiver: joi + .object({ + did: addressSchema.required(), + kind: joi.string().valid('Default').required(), + }) + .required(), + assetId: joi.string().required(), + amount: joi.string().required(), + }) + .required(), + }) + ) + .required(), + portfolios: joi + .array() + .items( + joi.object({ + did: addressSchema.required(), + kind: joi.string().valid('Default').required(), + }) + ) + .required(), + instructionMemo: joi.string().required(), + mediators: joi.array().length(0).required(), +}); + // For standalone bondExtra transactions export const BondExtraTransactionSchema = joi.object({ value: joi.string().required(), diff --git a/modules/sdk-coin-polyx/test/resources/index.ts b/modules/sdk-coin-polyx/test/resources/index.ts index b3a4a69982..e864e26080 100644 --- a/modules/sdk-coin-polyx/test/resources/index.ts +++ b/modules/sdk-coin-polyx/test/resources/index.ts @@ -47,6 +47,13 @@ export const accounts = { publicKey: 'b5dda9aad5129b4c77b84bb08e662fc1c863a6a16f557a734375edad6d3ccbf7', address: '2GZNcm36nErZD3mi619C2iqYWQBRMmeWky9cLnbd2DcAvdxm', }, + rbitgoTokenOwner: { + mnemonic: 'size merry rare ***ory egg aff** orbit mu****** angry miss of*** bu******', + secretKey: + '8a09aff194b11b9640d3fe966c056a9ec5aa986a6b9ee66dad1b726f6f35919b96c86fef593f9d35ea70dd519e80dcf50d93630954f8d5c83414ae731129655f', + publicKey: '96c86fef593f9d35ea70dd519e80dcf50d93630954f8d5c83414ae731129655f', + address: '5FUQaif4s79d8sibGNdfLHuiyCqh2EmDpyWkjj8yQ5GtUpMV', + }, }; export const rawTx = { diff --git a/modules/sdk-coin-polyx/test/unit/polyx.ts b/modules/sdk-coin-polyx/test/unit/polyx.ts index 55a318a020..6cb15afb6e 100644 --- a/modules/sdk-coin-polyx/test/unit/polyx.ts +++ b/modules/sdk-coin-polyx/test/unit/polyx.ts @@ -1,4 +1,4 @@ -import should = require('should'); +import should from 'should'; import { TestBitGo, TestBitGoAPI } from '@bitgo/sdk-test'; import { BitGoAPI } from '@bitgo/sdk-api'; import { Polyx, Tpolyx } from '../../src'; diff --git a/modules/sdk-coin-polyx/test/unit/transactionBuilder/preApproveAssetBuilder.ts b/modules/sdk-coin-polyx/test/unit/transactionBuilder/preApproveAssetBuilder.ts new file mode 100644 index 0000000000..3a33f12408 --- /dev/null +++ b/modules/sdk-coin-polyx/test/unit/transactionBuilder/preApproveAssetBuilder.ts @@ -0,0 +1,122 @@ +import should from 'should'; +import { PreApproveAssetBuilder } from '../../../src/lib'; +import { utils } from '../../../src'; + +import { accounts, rawTx, chainName, genesisHash, mockTssSignature } from '../../resources'; +import { buildTestConfig } from './base'; +import { testnetMaterial } from '../../../src/resources'; + +describe('Polyx Pre Approve Asset Builder - Testnet', () => { + let builder: PreApproveAssetBuilder; + + const sender = accounts.rbitgoTokenOwner; + const assetId = '0x2ffe769d862a89948e1ccf1423bfc7f8'; + + beforeEach(() => { + const config = buildTestConfig(); + builder = new PreApproveAssetBuilder(config).material(utils.getMaterial(config.network.type)); + }); + + describe('build preApproveAsset transaction', () => { + it('should build a preApproveAsset transaction', async () => { + builder + .assetId(assetId) + .sender({ address: sender.address }) + .validity({ firstValid: 3933, maxDuration: 64 }) + .referenceBlock('0x149799bc9602cb5cf201f3425fb8d253b2d4e61fc119dcab3249f307f594754d') + .sequenceId({ name: 'Nonce', keyword: 'nonce', value: 200 }) + .fee({ amount: 0, type: 'tip' }); + builder.addSignature({ pub: sender.publicKey }, Buffer.from(mockTssSignature, 'hex')); + const tx = await builder.build(); + const txJson = tx.toJson(); + should.deepEqual(txJson.assetId, assetId); + should.deepEqual(txJson.sender, sender.address); + should.deepEqual(txJson.blockNumber, 3933); + should.deepEqual(txJson.referenceBlock, '0x149799bc9602cb5cf201f3425fb8d253b2d4e61fc119dcab3249f307f594754d'); + should.deepEqual(txJson.genesisHash, genesisHash); + should.deepEqual(txJson.specVersion, Number(testnetMaterial.specVersion)); + should.deepEqual(txJson.nonce, 200); + should.deepEqual(txJson.tip, 0); + should.deepEqual(txJson.transactionVersion, Number(testnetMaterial.txVersion)); + should.deepEqual(txJson.chainName, testnetMaterial.chainName); + should.deepEqual(txJson.eraPeriod, 64); + + const inputs = tx.inputs[0]; + should.deepEqual(inputs.address, sender.address); + should.deepEqual(inputs.value, '0'); + + const outputs = tx.outputs[0]; + should.deepEqual(outputs.address, sender.address); + should.deepEqual(outputs.value, '0'); + }); + + it('should build an unsigned preApproveAsset transaction', async () => { + builder + .assetId(assetId) + .sender({ address: sender.address }) + .validity({ firstValid: 3933, maxDuration: 64 }) + .referenceBlock('0x149799bc9602cb5cf201f3425fb8d253b2d4e61fc119dcab3249f307f594754d') + .sequenceId({ name: 'Nonce', keyword: 'nonce', value: 200 }) + .fee({ amount: 0, type: 'tip' }); + const tx = await builder.build(); + const txJson = tx.toJson(); + should.deepEqual(txJson.amount, '0'); + should.deepEqual(txJson.assetId, assetId); + should.deepEqual(txJson.sender, sender.address); + should.deepEqual(txJson.blockNumber, 3933); + should.deepEqual(txJson.referenceBlock, '0x149799bc9602cb5cf201f3425fb8d253b2d4e61fc119dcab3249f307f594754d'); + should.deepEqual(txJson.genesisHash, genesisHash); + should.deepEqual(txJson.specVersion, Number(testnetMaterial.specVersion)); + should.deepEqual(txJson.nonce, 200); + should.deepEqual(txJson.tip, 0); + should.deepEqual(txJson.transactionVersion, Number(testnetMaterial.txVersion)); + should.deepEqual(txJson.chainName, chainName); + should.deepEqual(txJson.eraPeriod, 64); + }); + + it.skip('should build from raw signed tx', async () => { + builder.from(rawTx.cddTransaction.signed); + builder + .validity({ firstValid: 3933, maxDuration: 64 }) + .referenceBlock('0x149799bc9602cb5cf201f3425fb8d253b2d4e61fc119dcab3249f307f594754d'); + const tx = await builder.build(); + const txJson = tx.toJson(); + should.deepEqual(txJson.amount, '0'); + should.deepEqual(txJson.to, '5EFWg5wKTgkFE9XCxigBYPYKQg173djwSmRbkALCdL1jFVUU'); + should.deepEqual(txJson.sender, '5E7XWJRysj27EzibT4duRxrBQT9Qfa7Z5nAAvJmvd32nhkjH'); + should.deepEqual(txJson.blockNumber, 3933); + should.deepEqual(txJson.referenceBlock, '0x149799bc9602cb5cf201f3425fb8d253b2d4e61fc119dcab3249f307f594754d'); + should.deepEqual(txJson.genesisHash, genesisHash); + should.deepEqual(txJson.specVersion, Number(testnetMaterial.specVersion)); + should.deepEqual(txJson.nonce, 1); + should.deepEqual(txJson.tip, 0); + should.deepEqual(txJson.transactionVersion, Number(testnetMaterial.txVersion)); + should.deepEqual(txJson.chainName, chainName); + should.deepEqual(txJson.eraPeriod, 64); + }); + + it.skip('should build from raw unsigned tx', async () => { + builder.from(rawTx.cddTransaction.unsigned); + builder + .validity({ firstValid: 3933, maxDuration: 64 }) + .referenceBlock('0x149799bc9602cb5cf201f3425fb8d253b2d4e61fc119dcab3249f307f594754d') + .sender({ address: sender.address }) + .addSignature({ pub: sender.publicKey }, Buffer.from(mockTssSignature, 'hex')); + + const tx = await builder.build(); + const txJson = tx.toJson(); + should.deepEqual(txJson.amount, '0'); + should.deepEqual(txJson.to, '5EFWg5wKTgkFE9XCxigBYPYKQg173djwSmRbkALCdL1jFVUU'); + should.deepEqual(txJson.sender, sender.address); + should.deepEqual(txJson.blockNumber, 3933); + should.deepEqual(txJson.referenceBlock, '0x149799bc9602cb5cf201f3425fb8d253b2d4e61fc119dcab3249f307f594754d'); + should.deepEqual(txJson.genesisHash, genesisHash); + should.deepEqual(txJson.specVersion, Number(testnetMaterial.specVersion)); + should.deepEqual(txJson.nonce, 1); + should.deepEqual(txJson.eraPeriod, 64); + should.deepEqual(txJson.tip, 0); + should.deepEqual(txJson.transactionVersion, Number(testnetMaterial.txVersion)); + should.deepEqual(txJson.chainName, chainName); + }); + }); +}); diff --git a/modules/sdk-coin-polyx/test/unit/transactionBuilder/tokenTransferBuilder.ts b/modules/sdk-coin-polyx/test/unit/transactionBuilder/tokenTransferBuilder.ts new file mode 100644 index 0000000000..78e8132643 --- /dev/null +++ b/modules/sdk-coin-polyx/test/unit/transactionBuilder/tokenTransferBuilder.ts @@ -0,0 +1,139 @@ +import should from 'should'; +import { TokenTransferBuilder } from '../../../src/lib'; +import { utils } from '../../../src'; + +import { accounts, rawTx, chainName, genesisHash, mockTssSignature } from '../../resources'; +import { buildTestConfig } from './base'; +import { testnetMaterial } from '../../../src/resources'; + +describe('Polyx token transfer Builder - Testnet', () => { + let builder: TokenTransferBuilder; + + const sender = accounts.rbitgoTokenOwner; + const senderDID = '0x28e8649fec23dd688090b9b5bb950fd34bf20a014cf05542e3ad0264915ee775'; + const receiverDID = '0x9202856204a721d2f5e8b85408067d54f1ca84390bf4f558b5615a5a6d3bddb8'; + const assetId = '0x2ffe769d862a89948e1ccf1423bfc7f8'; + + beforeEach(() => { + const config = buildTestConfig(); + builder = new TokenTransferBuilder(config).material(utils.getMaterial(config.network.type)); + }); + + describe('build tokenTransfer transaction', () => { + it('should build a tokenTransfer transaction', async () => { + builder + .toDID(receiverDID) + .fromDID(senderDID) + .memo('0') + .assetId(assetId) + .amount('100') + .sender({ address: sender.address }) + .validity({ firstValid: 3933, maxDuration: 64 }) + .referenceBlock('0x149799bc9602cb5cf201f3425fb8d253b2d4e61fc119dcab3249f307f594754d') + .sequenceId({ name: 'Nonce', keyword: 'nonce', value: 200 }) + .fee({ amount: 0, type: 'tip' }); + builder.addSignature({ pub: sender.publicKey }, Buffer.from(mockTssSignature, 'hex')); + const tx = await builder.build(); + const txJson = tx.toJson(); + should.deepEqual(txJson.amount, '100'); + should.deepEqual(txJson.toDID, receiverDID); + should.deepEqual(txJson.fromDID, senderDID); + should.deepEqual(txJson.sender, sender.address); + should.deepEqual(txJson.assetId, assetId); + should.deepEqual(txJson.memo, '0x3030303030303030303030303030303030303030303030303030303030303030'); + should.deepEqual(txJson.blockNumber, 3933); + should.deepEqual(txJson.referenceBlock, '0x149799bc9602cb5cf201f3425fb8d253b2d4e61fc119dcab3249f307f594754d'); + should.deepEqual(txJson.genesisHash, genesisHash); + should.deepEqual(txJson.specVersion, Number(testnetMaterial.specVersion)); + should.deepEqual(txJson.nonce, 200); + should.deepEqual(txJson.tip, 0); + should.deepEqual(txJson.transactionVersion, Number(testnetMaterial.txVersion)); + should.deepEqual(txJson.chainName, testnetMaterial.chainName); + should.deepEqual(txJson.eraPeriod, 64); + + const inputs = tx.inputs[0]; + should.deepEqual(inputs.address, senderDID); + should.deepEqual(inputs.value, '100'); + + const outputs = tx.outputs[0]; + should.deepEqual(outputs.address, receiverDID); + should.deepEqual(outputs.value, '100'); + }); + + it('should build an unsigned tokenTransfer transaction', async () => { + builder + .toDID(receiverDID) + .fromDID(senderDID) + .memo('0') + .assetId(assetId) + .amount('100') + .sender({ address: sender.address }) + .validity({ firstValid: 3933, maxDuration: 64 }) + .referenceBlock('0x149799bc9602cb5cf201f3425fb8d253b2d4e61fc119dcab3249f307f594754d') + .sequenceId({ name: 'Nonce', keyword: 'nonce', value: 200 }) + .fee({ amount: 0, type: 'tip' }); + const tx = await builder.build(); + const txJson = tx.toJson(); + should.deepEqual(txJson.amount, '100'); + should.deepEqual(txJson.toDID, receiverDID); + should.deepEqual(txJson.fromDID, senderDID); + should.deepEqual(txJson.sender, sender.address); + should.deepEqual(txJson.assetId, assetId); + should.deepEqual(txJson.memo, '0x3030303030303030303030303030303030303030303030303030303030303030'); + should.deepEqual(txJson.blockNumber, 3933); + should.deepEqual(txJson.referenceBlock, '0x149799bc9602cb5cf201f3425fb8d253b2d4e61fc119dcab3249f307f594754d'); + should.deepEqual(txJson.genesisHash, genesisHash); + should.deepEqual(txJson.specVersion, Number(testnetMaterial.specVersion)); + should.deepEqual(txJson.nonce, 200); + should.deepEqual(txJson.tip, 0); + should.deepEqual(txJson.transactionVersion, Number(testnetMaterial.txVersion)); + should.deepEqual(txJson.chainName, chainName); + should.deepEqual(txJson.eraPeriod, 64); + }); + + it.skip('should build from raw signed tx', async () => { + builder.from(rawTx.cddTransaction.signed); + builder + .validity({ firstValid: 3933, maxDuration: 64 }) + .referenceBlock('0x149799bc9602cb5cf201f3425fb8d253b2d4e61fc119dcab3249f307f594754d'); + const tx = await builder.build(); + const txJson = tx.toJson(); + should.deepEqual(txJson.amount, '0'); + should.deepEqual(txJson.to, '5EFWg5wKTgkFE9XCxigBYPYKQg173djwSmRbkALCdL1jFVUU'); + should.deepEqual(txJson.sender, '5E7XWJRysj27EzibT4duRxrBQT9Qfa7Z5nAAvJmvd32nhkjH'); + should.deepEqual(txJson.blockNumber, 3933); + should.deepEqual(txJson.referenceBlock, '0x149799bc9602cb5cf201f3425fb8d253b2d4e61fc119dcab3249f307f594754d'); + should.deepEqual(txJson.genesisHash, genesisHash); + should.deepEqual(txJson.specVersion, Number(testnetMaterial.specVersion)); + should.deepEqual(txJson.nonce, 1); + should.deepEqual(txJson.tip, 0); + should.deepEqual(txJson.transactionVersion, Number(testnetMaterial.txVersion)); + should.deepEqual(txJson.chainName, chainName); + should.deepEqual(txJson.eraPeriod, 64); + }); + + it.skip('should build from raw unsigned tx', async () => { + builder.from(rawTx.cddTransaction.unsigned); + builder + .validity({ firstValid: 3933, maxDuration: 64 }) + .referenceBlock('0x149799bc9602cb5cf201f3425fb8d253b2d4e61fc119dcab3249f307f594754d') + .sender({ address: sender.address }) + .addSignature({ pub: sender.publicKey }, Buffer.from(mockTssSignature, 'hex')); + + const tx = await builder.build(); + const txJson = tx.toJson(); + should.deepEqual(txJson.amount, '0'); + should.deepEqual(txJson.to, '5EFWg5wKTgkFE9XCxigBYPYKQg173djwSmRbkALCdL1jFVUU'); + should.deepEqual(txJson.sender, sender.address); + should.deepEqual(txJson.blockNumber, 3933); + should.deepEqual(txJson.referenceBlock, '0x149799bc9602cb5cf201f3425fb8d253b2d4e61fc119dcab3249f307f594754d'); + should.deepEqual(txJson.genesisHash, genesisHash); + should.deepEqual(txJson.specVersion, Number(testnetMaterial.specVersion)); + should.deepEqual(txJson.nonce, 1); + should.deepEqual(txJson.eraPeriod, 64); + should.deepEqual(txJson.tip, 0); + should.deepEqual(txJson.transactionVersion, Number(testnetMaterial.txVersion)); + should.deepEqual(txJson.chainName, chainName); + }); + }); +}); diff --git a/modules/sdk-coin-tao/test/unit/tao.ts b/modules/sdk-coin-tao/test/unit/tao.ts index 8aa0bf706e..ce5411187e 100644 --- a/modules/sdk-coin-tao/test/unit/tao.ts +++ b/modules/sdk-coin-tao/test/unit/tao.ts @@ -1,4 +1,4 @@ -import should = require('should'); +import should from 'should'; import { TestBitGo, TestBitGoAPI } from '@bitgo/sdk-test'; import { BitGoAPI } from '@bitgo/sdk-api'; import { Tao, Ttao } from '../../src';