diff --git a/modules/sdk-coin-polyx/src/lib/iface.ts b/modules/sdk-coin-polyx/src/lib/iface.ts index 5b007db8fe..65cb3b549d 100644 --- a/modules/sdk-coin-polyx/src/lib/iface.ts +++ b/modules/sdk-coin-polyx/src/lib/iface.ts @@ -12,6 +12,8 @@ export interface TxData extends Interface.TxData { assetId?: string; fromDID?: string; toDID?: string; + instructionId?: string; + portfolioDID?: string; } /** @@ -49,6 +51,8 @@ export const MethodNames = { PreApproveAsset: 'preApproveAsset' as const, AddAndAffirmWithMediators: 'addAndAffirmWithMediators' as const, + + RejectInstruction: 'rejectInstruction' as const, } as const; // Create a type that represents the keys of this object @@ -85,6 +89,12 @@ export interface AddAndAffirmWithMediatorsArgs extends Args { mediators: []; } +export interface RejectInstructionBuilderArgs extends Args { + id: string; + portfolio: { did: string; kind: PortfolioKind.Default }; + numberOfAssets: { fungible: number; nonFungible: number; offChain: number }; +} + export interface TxMethod extends Omit { args: | Interface.TransferArgs @@ -100,7 +110,8 @@ export interface TxMethod extends Omit { | Interface.BatchArgs | RegisterDidWithCDDArgs | PreApproveAssetArgs - | AddAndAffirmWithMediatorsArgs; + | AddAndAffirmWithMediatorsArgs + | RejectInstructionBuilderArgs; name: MethodNamesValues; } diff --git a/modules/sdk-coin-polyx/src/lib/index.ts b/modules/sdk-coin-polyx/src/lib/index.ts index e33bdeec8f..2c936f4363 100644 --- a/modules/sdk-coin-polyx/src/lib/index.ts +++ b/modules/sdk-coin-polyx/src/lib/index.ts @@ -14,6 +14,7 @@ export { TransferBuilder } from './transferBuilder'; export { RegisterDidWithCDDBuilder } from './registerDidWithCDDBuilder'; export { PreApproveAssetBuilder } from './preApproveAssetBuilder'; export { TokenTransferBuilder } from './tokenTransferBuilder'; +export { RejectInstructionBuilder } from './rejectInstructionBuilder'; 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/rejectInstructionBuilder.ts b/modules/sdk-coin-polyx/src/lib/rejectInstructionBuilder.ts new file mode 100644 index 0000000000..eb01557f49 --- /dev/null +++ b/modules/sdk-coin-polyx/src/lib/rejectInstructionBuilder.ts @@ -0,0 +1,103 @@ +import { TransactionType } from '@bitgo/sdk-core'; +import { BaseCoin as CoinConfig } from '@bitgo/statics'; +import { PolyxBaseBuilder } from './baseBuilder'; +import { TxMethod, MethodNames, RejectInstructionBuilderArgs, PortfolioKind } from './iface'; +import { Transaction } from './transaction'; +import { Interface } from '@bitgo/abstract-substrate'; +import { RejectInstructionTransactionSchema } from './txnSchema'; +import { DecodedSignedTx, DecodedSigningPayload, defineMethod, UnsignedTransaction } from '@substrate/txwrapper-core'; + +export class RejectInstructionBuilder extends PolyxBaseBuilder { + protected _instructionId: string; + protected _portfolioDID: string; + + constructor(_coinConfig: Readonly) { + super(_coinConfig); + this._transaction = new Transaction(_coinConfig); + } + + protected get transactionType(): TransactionType { + return TransactionType.RejectInstruction; + } + + protected buildTransaction(): UnsignedTransaction { + const baseTxInfo = this.createBaseTxInfo(); + return this.rejectInstruction( + { + id: this._instructionId, + portfolio: { + did: this._portfolioDID, + kind: PortfolioKind.Default, + }, + numberOfAssets: { + fungible: 1, + nonFungible: 0, + offChain: 0, + }, + }, + baseTxInfo + ); + } + + /** + * @param instructionId - The ID of the instruction to be rejected + * @returns {this} + */ + instructionId(instructionId: string): this { + this._instructionId = instructionId; + return this; + } + + /** + * @param portfolioDID - The DID of the portfolio associated with the instruction + * @returns {this} + */ + portfolioDID(portfolioDID: string): this { + this._portfolioDID = portfolioDID; + return this; + } + + /** @inheritdoc */ + protected fromImplementation(rawTransaction: string): Transaction { + const tx = super.fromImplementation(rawTransaction); + if (this._method?.name === MethodNames.RejectInstruction) { + const txMethod = this._method.args as RejectInstructionBuilderArgs; + this._instructionId = txMethod.id as string; + this._portfolioDID = txMethod.portfolio.did as string; + } else { + throw new Error(`Cannot build from transaction with method ${this._method?.name} for RejectInstructionBuilder`); + } + return tx; + } + + /** @inheritdoc */ + validateDecodedTransaction(decodedTxn: DecodedSigningPayload | DecodedSignedTx, rawTransaction?: string): void { + if (decodedTxn.method?.name === MethodNames.RejectInstruction) { + const txMethod = decodedTxn.method.args as RejectInstructionBuilderArgs; + const id = txMethod.id; + const portfolio = txMethod.portfolio; + + const validationResult = RejectInstructionTransactionSchema.validate({ + id, + portfolio, + }); + if (validationResult.error) { + throw new Error(`Invalid transaction: ${validationResult.error.message}`); + } + } + } + + private rejectInstruction(args: RejectInstructionBuilderArgs, info: Interface.CreateBaseTxInfo): UnsignedTransaction { + return defineMethod( + { + method: { + args, + name: 'rejectInstruction', + pallet: 'settlement', + }, + ...info.baseTxInfo, + }, + info.options + ); + } +} diff --git a/modules/sdk-coin-polyx/src/lib/tokenTransferBuilder.ts b/modules/sdk-coin-polyx/src/lib/tokenTransferBuilder.ts index 3851ab44e6..741ec39513 100644 --- a/modules/sdk-coin-polyx/src/lib/tokenTransferBuilder.ts +++ b/modules/sdk-coin-polyx/src/lib/tokenTransferBuilder.ts @@ -137,7 +137,6 @@ export class TokenTransferBuilder extends PolyxBaseBuilder { + let builder: RejectInstructionBuilder; + + const sender = accounts.account1; + const instructionId = '14100'; + const portfolioDID = '0x1208d7851e6698249aea40742701ee1ef6cdcced260a7c49c1cca1a9db836342'; + + beforeEach(() => { + const config = buildTestConfig(); + builder = new RejectInstructionBuilder(config).material(utils.getMaterial(config.network.type)); + }); + + describe('build rejectInstruction transaction', () => { + it('should build a rejectInstruction transaction', async () => { + builder + .instructionId(instructionId) + .portfolioDID(portfolioDID) + .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.instructionId, instructionId); + should.deepEqual(txJson.portfolioDID, portfolioDID); + 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); + }); + + it('should build an unsigned rejectInstruction transaction', async () => { + builder + .instructionId(instructionId) + .portfolioDID(portfolioDID) + .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.instructionId, instructionId); + should.deepEqual(txJson.portfolioDID, portfolioDID); + 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('should build from raw signed tx', async () => { + if (rawTx.rejectInstruction?.signed) { + builder.from(rawTx.rejectInstruction.signed); + builder + .validity({ firstValid: 3933, maxDuration: 64 }) + .referenceBlock('0x149799bc9602cb5cf201f3425fb8d253b2d4e61fc119dcab3249f307f594754d'); + const tx = await builder.build(); + const txJson = tx.toJson(); + should.exist(txJson.instructionId); + should.deepEqual(txJson.instructionId, instructionId); + should.exist(txJson.portfolioDID); + should.deepEqual(txJson.portfolioDID, portfolioDID); + 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.transactionVersion, Number(testnetMaterial.txVersion)); + should.deepEqual(txJson.chainName, chainName); + should.deepEqual(txJson.eraPeriod, 64); + } + }); + + it('should build from raw unsigned tx', async () => { + if (rawTx.rejectInstruction?.unsigned) { + builder.from(rawTx.rejectInstruction.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.exist(txJson.instructionId); + should.deepEqual(txJson.instructionId, instructionId); + should.exist(txJson.portfolioDID); + should.deepEqual(txJson.portfolioDID, portfolioDID); + 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.eraPeriod, 64); + should.deepEqual(txJson.transactionVersion, Number(testnetMaterial.txVersion)); + should.deepEqual(txJson.chainName, chainName); + } + }); + + it('should validate instruction ID is set', async () => { + builder + .portfolioDID(portfolioDID) + .sender({ address: sender.address }) + .validity({ firstValid: 3933, maxDuration: 64 }) + .referenceBlock('0x149799bc9602cb5cf201f3425fb8d253b2d4e61fc119dcab3249f307f594754d') + .sequenceId({ name: 'Nonce', keyword: 'nonce', value: 200 }) + .fee({ amount: 0, type: 'tip' }); + + await builder.build().should.be.rejected(); + }); + }); +}); diff --git a/modules/sdk-core/src/account-lib/baseCoin/enum.ts b/modules/sdk-core/src/account-lib/baseCoin/enum.ts index f5cb498b58..89c2fd8ab0 100644 --- a/modules/sdk-core/src/account-lib/baseCoin/enum.ts +++ b/modules/sdk-core/src/account-lib/baseCoin/enum.ts @@ -107,6 +107,9 @@ export enum TransactionType { // cspr stakingUnlock, + + // polyx + RejectInstruction, } /**