diff --git a/modules/sdk-coin-xlm/src/xlm.ts b/modules/sdk-coin-xlm/src/xlm.ts index 02d56a71c7..e2db57090c 100644 --- a/modules/sdk-coin-xlm/src/xlm.ts +++ b/modules/sdk-coin-xlm/src/xlm.ts @@ -124,6 +124,15 @@ interface TransactionOperation { coin: string; limit?: string; asset?: stellar.Asset; + // accountConfig (setOptions) operation fields + setFlags?: number; + clearFlags?: number; + // authorizeTrustline (setTrustLineFlags) operation fields + trustor?: string; + authorized?: boolean; + // clawback operation fields + from?: string; + amount?: string; } interface TransactionOutput extends BaseTransactionOutput { @@ -152,6 +161,65 @@ interface VerifyTransactionOptions extends BaseVerifyTransactionOptions { txParams: TransactionParams; } +export interface AccountFlags { + authRequired?: boolean; + authRevocable?: boolean; + authClawbackEnabled?: boolean; +} + +export interface TrustlineAuthEntry { + trustor: string; + token: string; // 'xlm:ASSETCODE-ISSUERADDRESS' + authorize: boolean; +} + +export interface ClawbackEntry { + from: string; + token: string; // 'xlm:ASSETCODE-ISSUERADDRESS' + amount: string; // base units (stroops) +} + +export interface AccountConfigVerifyParams extends BuildOptions { + type: 'accountConfig'; + flags?: AccountFlags; + clearFlags?: AccountFlags; +} + +export interface AuthorizeTrustlineVerifyParams extends BuildOptions { + type: 'authorizeTrustline'; + trustlineAuths: TrustlineAuthEntry[]; +} + +export interface ClawbackVerifyParams extends BuildOptions { + type: 'clawback'; + clawbacks: ClawbackEntry[]; +} + +function isAccountConfigVerifyParams(params: unknown): params is AccountConfigVerifyParams { + if (typeof params !== 'object' || params === null) return false; + const p = params as Record; + // flags and clearFlags are optional, but must be plain objects when present + return ( + p.type === 'accountConfig' && + (p.flags === undefined || (typeof p.flags === 'object' && p.flags !== null)) && + (p.clearFlags === undefined || (typeof p.clearFlags === 'object' && p.clearFlags !== null)) + ); +} + +function isAuthorizeTrustlineVerifyParams(params: unknown): params is AuthorizeTrustlineVerifyParams { + if (typeof params !== 'object' || params === null) return false; + const p = params as Record; + // trustlineAuths is required and must be an array + return p.type === 'authorizeTrustline' && Array.isArray(p.trustlineAuths); +} + +function isClawbackVerifyParams(params: unknown): params is ClawbackVerifyParams { + if (typeof params !== 'object' || params === null) return false; + const p = params as Record; + // clawbacks is required and must be an array + return p.type === 'clawback' && Array.isArray(p.clawbacks); +} + export class Xlm extends BaseCoin { public readonly homeDomain: string; public static readonly tokenPatternSeparator = '-'; // separator for token code and issuer @@ -625,7 +693,9 @@ export class Xlm extends BaseCoin { */ async getExtraPrebuildParams(buildParams: ExtraPrebuildParamsOptions): Promise { const params: { recipients?: Record[] } = {}; - if (buildParams.type === 'trustline') { + const nonPaymentTypes = ['trustline', 'accountConfig', 'authorizeTrustline', 'clawback']; + // Non-payment transaction types must not inject a default recipient + if (nonPaymentTypes.includes(buildParams.type as string)) { params.recipients = []; } return params; @@ -961,6 +1031,30 @@ export class Xlm extends BaseCoin { asset, limit: this.bigUnitsToBaseUnits(op.limit), }); + } else if (op.type === 'setOptions') { + const setOptionsOp = op as stellar.Operation.SetOptions; + operations.push({ + type: 'accountConfig', + coin: this.getChain(), + setFlags: setOptionsOp.setFlags, + clearFlags: setOptionsOp.clearFlags, + }); + } else if (op.type === 'setTrustLineFlags') { + const setTrustLineFlagsOp = op as stellar.Operation.SetTrustLineFlags; + operations.push({ + type: 'authorizeTrustline', + coin: this.getTokenNameFromStellarAsset(setTrustLineFlagsOp.asset), + trustor: setTrustLineFlagsOp.trustor, + authorized: setTrustLineFlagsOp.flags?.authorized, + }); + } else if (op.type === 'clawback') { + const clawbackOp = op as stellar.Operation.Clawback; + operations.push({ + type: 'clawback', + coin: this.getTokenNameFromStellarAsset(clawbackOp.asset), + from: clawbackOp.from, + amount: this.bigUnitsToBaseUnits(clawbackOp.amount), + }); } }); @@ -1066,6 +1160,105 @@ export class Xlm extends BaseCoin { }); } + private verifyAccountConfigOperations(operations: stellar.Operation[], txParams: AccountConfigVerifyParams): void { + const setOptionsOps = operations.filter( + (operation): operation is stellar.Operation.SetOptions => operation.type === 'setOptions' + ); + if (setOptionsOps.length === 0) { + throw new Error('accountConfig transaction must contain at least one setOptions operation'); + } + // Reject multiple setOptions ops: txParams only carries one flags/clearFlags set, so any + // additional ops would be silently unverified — a verification gap. + if (setOptionsOps.length > 1) { + throw new Error('accountConfig transaction must contain exactly one setOptions operation'); + } + const setOptionsOp = setOptionsOps[0]; + + if (txParams.flags) { + const expectedSetFlag = ((txParams.flags.authRequired ? stellar.AuthRequiredFlag : 0) | + (txParams.flags.authRevocable ? stellar.AuthRevocableFlag : 0) | + (txParams.flags.authClawbackEnabled ? stellar.AuthClawbackEnabledFlag : 0)) as number; + if (setOptionsOp.setFlags !== expectedSetFlag) { + throw new Error(`accountConfig setFlags mismatch: expected ${expectedSetFlag}, got ${setOptionsOp.setFlags}`); + } + } + + if (txParams.clearFlags) { + const expectedClearFlag = ((txParams.clearFlags.authRequired ? stellar.AuthRequiredFlag : 0) | + (txParams.clearFlags.authRevocable ? stellar.AuthRevocableFlag : 0) | + (txParams.clearFlags.authClawbackEnabled ? stellar.AuthClawbackEnabledFlag : 0)) as number; + if (setOptionsOp.clearFlags !== expectedClearFlag) { + throw new Error( + `accountConfig clearFlags mismatch: expected ${expectedClearFlag}, got ${setOptionsOp.clearFlags}` + ); + } + } + } + + private verifyAuthorizeTrustlineOperations( + operations: stellar.Operation[], + txParams: AuthorizeTrustlineVerifyParams + ): void { + const trustLineFlagOps = operations.filter( + (operation): operation is stellar.Operation.SetTrustLineFlags => operation.type === 'setTrustLineFlags' + ); + if (trustLineFlagOps.length !== txParams.trustlineAuths.length) { + throw new Error( + `authorizeTrustline operation count mismatch: expected ${txParams.trustlineAuths.length}, got ${trustLineFlagOps.length}` + ); + } + txParams.trustlineAuths.forEach((trustlineAuth, index) => { + const trustLineFlagOp = trustLineFlagOps[index]; + if (trustLineFlagOp.trustor !== trustlineAuth.trustor) { + throw new Error(`authorizeTrustline trustor mismatch at index ${index}`); + } + const actualToken = this.getTokenNameFromStellarAsset(trustLineFlagOp.asset); + if (actualToken !== trustlineAuth.token) { + throw new Error( + `authorizeTrustline token mismatch at index ${index}: expected ${trustlineAuth.token}, got ${actualToken}` + ); + } + const actualAuthorized = trustLineFlagOp.flags?.authorized; + if (actualAuthorized !== trustlineAuth.authorize) { + throw new Error( + `authorizeTrustline authorize mismatch at index ${index}: expected ${trustlineAuth.authorize}, got ${actualAuthorized}` + ); + } + }); + } + + private verifyClawbackOperations(operations: stellar.Operation[], txParams: ClawbackVerifyParams): void { + const clawbackOps = operations.filter( + (operation): operation is stellar.Operation.Clawback => operation.type === 'clawback' + ); + if (clawbackOps.length !== txParams.clawbacks.length) { + throw new Error( + `clawback operation count mismatch: expected ${txParams.clawbacks.length}, got ${clawbackOps.length}` + ); + } + txParams.clawbacks.forEach((clawbackEntry, index) => { + const clawbackOp = clawbackOps[index]; + if (clawbackOp.from !== clawbackEntry.from) { + throw new Error(`clawback 'from' address mismatch at index ${index}`); + } + const actualToken = this.getTokenNameFromStellarAsset(clawbackOp.asset); + if (actualToken !== clawbackEntry.token) { + throw new Error( + `clawback token mismatch at index ${index}: expected ${clawbackEntry.token}, got ${actualToken}` + ); + } + const actualAmountInBaseUnits = new BigNumber(this.bigUnitsToBaseUnits(clawbackOp.amount)); + const expectedAmountInBaseUnits = new BigNumber(clawbackEntry.amount); + if (!actualAmountInBaseUnits.eq(expectedAmountInBaseUnits)) { + throw new Error( + `clawback amount mismatch at index ${index}: expected ${ + clawbackEntry.amount + }, got ${actualAmountInBaseUnits.toFixed()}` + ); + } + }); + } + getRecipientOrThrow(txParams: TransactionParams): ITransactionRecipient { if (!txParams.recipients || txParams.recipients.length === 0) throw new Error('Missing recipients on token enablement'); @@ -1199,6 +1392,12 @@ export class Xlm extends BaseCoin { this.verifyTokenLimits(txParams, trustlineOperations); } else if (txParams.type === 'trustline') { this.verifyTrustlineTxOperations(tx.operations, txParams); + } else if (isAccountConfigVerifyParams(txParams)) { + this.verifyAccountConfigOperations(tx.operations, txParams); + } else if (isAuthorizeTrustlineVerifyParams(txParams)) { + this.verifyAuthorizeTrustlineOperations(tx.operations, txParams); + } else if (isClawbackVerifyParams(txParams)) { + this.verifyClawbackOperations(tx.operations, txParams); } else { if (_.isEmpty(outputOperations)) { throw new Error('transaction prebuild does not have any operations'); diff --git a/modules/sdk-coin-xlm/test/unit/xlm.ts b/modules/sdk-coin-xlm/test/unit/xlm.ts index 859da463e3..69c1b41a2b 100644 --- a/modules/sdk-coin-xlm/test/unit/xlm.ts +++ b/modules/sdk-coin-xlm/test/unit/xlm.ts @@ -1245,4 +1245,372 @@ describe('XLM:', function () { ); }); }); + + describe('getExtraPrebuildParams for non-payment types', () => { + // Non-payment intents (accountConfig/authorizeTrustline/clawback) must inject an empty + // recipients array so the platform build path does not synthesize a default payment recipient. + it('should return empty recipients for accountConfig', async () => { + const extraParams = await basecoin.getExtraPrebuildParams({ type: 'accountConfig' }); + extraParams.should.deepEqual({ recipients: [] }); + }); + + it('should return empty recipients for authorizeTrustline', async () => { + const extraParams = await basecoin.getExtraPrebuildParams({ type: 'authorizeTrustline' }); + extraParams.should.deepEqual({ recipients: [] }); + }); + + it('should return empty recipients for clawback', async () => { + const extraParams = await basecoin.getExtraPrebuildParams({ type: 'clawback' }); + extraParams.should.deepEqual({ recipients: [] }); + }); + + it('should not override recipients for a payment (no type) intent', async () => { + // A plain payment build has no `type`, so the method must return {} and leave + // recipients untouched rather than clobbering them with an empty array. + const extraParams = await basecoin.getExtraPrebuildParams({}); + extraParams.should.deepEqual({}); + }); + }); + + describe('verifyTransaction for accountConfig/authorizeTrustline/clawback', () => { + // txlm uses the Stellar TESTNET passphrase, so every transaction under test must be built + // and re-parsed against the same network the coin verifies with. + const stellarNetworkPassphrase = stellar.Networks.TESTNET; + const sourceAddress = 'GA34NPQ4M54HHZBKSDZ5B3J3BZHTXKCZD4UFO2OYZERPOASK4DAATSIB'; + const issuerAddress = 'GCWHAO4SVB4KX3Q62QZGZUHUH2GSH3OIV7IS7Y3MPQOFGQFGBP6IYCOU'; + const trustorAddress = 'GCNFRU774FPHLV3HAB6CR54XJYFYITOLU6KS2J5BNCLDPYN7I3DOMIPY'; + const bstAsset = new stellar.Asset('BST', issuerAddress); + const bstTokenName = `txlm:BST-${issuerAddress}`; + + // Builds an unsigned txlm transaction from the given operations and returns its base64 XDR. + // Unsigned is intentional: verifyTransaction only runs signature checks when signatures exist, + // so these tests exercise only the operation-verification branches. + const buildTxBase64 = (stellarOperations: stellar.xdr.Operation[]): string => { + const sourceAccount = new stellar.Account(sourceAddress, '1'); + const transactionBuilder = new stellar.TransactionBuilder(sourceAccount, { + fee: '100', + networkPassphrase: stellarNetworkPassphrase, + }); + stellarOperations.forEach((stellarOperation) => transactionBuilder.addOperation(stellarOperation)); + const builtTransaction = transactionBuilder.setTimeout(0).build(); + return builtTransaction.toEnvelope().toXDR('base64'); + }; + + describe('accountConfig', () => { + it('should verify a setOptions tx whose setFlags match the requested flags', async () => { + // authRequired (1) | authRevocable (2) => setFlags 3 on the parsed operation + const txPrebuild = { + txBase64: buildTxBase64([ + stellar.Operation.setOptions({ + setFlags: (stellar.AuthRequiredFlag | stellar.AuthRevocableFlag) as stellar.AuthFlag, + }), + ]), + }; + const txParams = { type: 'accountConfig', flags: { authRequired: true, authRevocable: true } }; + const isValid = await basecoin.verifyTransaction({ txParams, txPrebuild, wallet: {}, verification: {} }); + isValid.should.equal(true); + }); + + it('should verify a setOptions tx whose clearFlags match the requested clearFlags', async () => { + // authClawbackEnabled (8) is the only cleared flag, so expected clearFlags is 8. + const txPrebuild = { + txBase64: buildTxBase64([stellar.Operation.setOptions({ clearFlags: stellar.AuthClawbackEnabledFlag })]), + }; + const txParams = { type: 'accountConfig', clearFlags: { authClawbackEnabled: true } }; + const isValid = await basecoin.verifyTransaction({ txParams, txPrebuild, wallet: {}, verification: {} }); + isValid.should.equal(true); + }); + + it('should throw setFlags mismatch when tx flags differ from requested flags', async () => { + // tx sets authRequired|authRevocable (3) but params only expect authRequired (1) => mismatch. + const txPrebuild = { + txBase64: buildTxBase64([ + stellar.Operation.setOptions({ + setFlags: (stellar.AuthRequiredFlag | stellar.AuthRevocableFlag) as stellar.AuthFlag, + }), + ]), + }; + const txParams = { type: 'accountConfig', flags: { authRequired: true } }; + await basecoin + .verifyTransaction({ txParams, txPrebuild, wallet: {}, verification: {} }) + .should.be.rejectedWith(/accountConfig setFlags mismatch/); + }); + + it('should throw when the tx contains no setOptions operation', async () => { + // A clawback op stands in for "some other op"; accountConfig verification requires setOptions. + const txPrebuild = { + txBase64: buildTxBase64([stellar.Operation.clawback({ from: trustorAddress, asset: bstAsset, amount: '1' })]), + }; + const txParams = { type: 'accountConfig', flags: { authRequired: true } }; + await basecoin + .verifyTransaction({ txParams, txPrebuild, wallet: {}, verification: {} }) + .should.be.rejectedWith(/must contain at least one setOptions/); + }); + + it('should throw when the tx contains more than one setOptions operation', async () => { + // Multiple setOptions ops cannot all be verified against the single flags/clearFlags spec in + // txParams — any extra op would be silently unverified, so we reject outright. + const txPrebuild = { + txBase64: buildTxBase64([ + stellar.Operation.setOptions({ setFlags: stellar.AuthRequiredFlag }), + stellar.Operation.setOptions({ setFlags: stellar.AuthRevocableFlag }), + ]), + }; + const txParams = { type: 'accountConfig', flags: { authRequired: true } }; + await basecoin + .verifyTransaction({ txParams, txPrebuild, wallet: {}, verification: {} }) + .should.be.rejectedWith(/must contain exactly one setOptions/); + }); + }); + + describe('authorizeTrustline', () => { + it('should verify a setTrustLineFlags tx when trustor and token match', async () => { + const txPrebuild = { + txBase64: buildTxBase64([ + stellar.Operation.setTrustLineFlags({ + trustor: trustorAddress, + asset: bstAsset, + flags: { authorized: true }, + }), + ]), + }; + const txParams = { + type: 'authorizeTrustline', + trustlineAuths: [{ trustor: trustorAddress, token: bstTokenName, authorize: true }], + }; + const isValid = await basecoin.verifyTransaction({ txParams, txPrebuild, wallet: {}, verification: {} }); + isValid.should.equal(true); + }); + + it('should throw count mismatch when op count differs from trustlineAuths count', async () => { + // Two setTrustLineFlags ops vs a single expected auth entry => count mismatch. + const txPrebuild = { + txBase64: buildTxBase64([ + stellar.Operation.setTrustLineFlags({ + trustor: trustorAddress, + asset: bstAsset, + flags: { authorized: true }, + }), + stellar.Operation.setTrustLineFlags({ + trustor: trustorAddress, + asset: bstAsset, + flags: { authorized: false }, + }), + ]), + }; + const txParams = { + type: 'authorizeTrustline', + trustlineAuths: [{ trustor: trustorAddress, token: bstTokenName, authorize: true }], + }; + await basecoin + .verifyTransaction({ txParams, txPrebuild, wallet: {}, verification: {} }) + .should.be.rejectedWith(/authorizeTrustline operation count mismatch/); + }); + + it('should throw trustor mismatch when the tx trustor differs from the requested trustor', async () => { + const differentTrustorAddress = 'GBRIS6W5OZNWWFJA6GYRF3JBK5WZNX5WWD2KC6NCOOIEMF7H6JMQLUI4'; + const txPrebuild = { + txBase64: buildTxBase64([ + stellar.Operation.setTrustLineFlags({ + trustor: trustorAddress, + asset: bstAsset, + flags: { authorized: true }, + }), + ]), + }; + const txParams = { + type: 'authorizeTrustline', + trustlineAuths: [{ trustor: differentTrustorAddress, token: bstTokenName, authorize: true }], + }; + await basecoin + .verifyTransaction({ txParams, txPrebuild, wallet: {}, verification: {} }) + .should.be.rejectedWith(/authorizeTrustline trustor mismatch/); + }); + + it('should throw authorize mismatch when the tx authorization state differs from the requested state', async () => { + const txPrebuild = { + txBase64: buildTxBase64([ + stellar.Operation.setTrustLineFlags({ + trustor: trustorAddress, + asset: bstAsset, + flags: { authorized: false }, + }), + ]), + }; + const txParams = { + type: 'authorizeTrustline', + trustlineAuths: [{ trustor: trustorAddress, token: bstTokenName, authorize: true }], + }; + await basecoin + .verifyTransaction({ txParams, txPrebuild, wallet: {}, verification: {} }) + .should.be.rejectedWith(/authorizeTrustline authorize mismatch/); + }); + + it('should not be treated as authorizeTrustline when trustlineAuths is not an array', async () => { + // The type guard requires trustlineAuths to be an array; a missing or non-array value means + // the params fail the guard and fall through to the default payment-verification path. + const txPrebuild = { + txBase64: buildTxBase64([ + stellar.Operation.setTrustLineFlags({ + trustor: trustorAddress, + asset: bstAsset, + flags: { authorized: true }, + }), + ]), + }; + const txParams = { type: 'authorizeTrustline' }; // trustlineAuths missing + await basecoin + .verifyTransaction({ txParams, txPrebuild, wallet: {}, verification: {} }) + .should.be.rejectedWith(/transaction prebuild does not have any operations/); + }); + }); + + describe('clawback', () => { + it('should verify a clawback tx when from, token and amount all match', async () => { + // Operation amount is in big units ('10' XLM), which the verifier converts to base units. + // 10 * 1e7 = 100000000 stroops, so the params amount must be expressed in base units. + const txPrebuild = { + txBase64: buildTxBase64([ + stellar.Operation.clawback({ from: trustorAddress, asset: bstAsset, amount: '10' }), + ]), + }; + const txParams = { + type: 'clawback', + clawbacks: [{ from: trustorAddress, token: bstTokenName, amount: '100000000' }], + }; + const isValid = await basecoin.verifyTransaction({ txParams, txPrebuild, wallet: {}, verification: {} }); + isValid.should.equal(true); + }); + + it('should throw amount mismatch when base-unit amount differs', async () => { + // tx claws back 100000000 base units but params expect 200000000 => mismatch. + const txPrebuild = { + txBase64: buildTxBase64([ + stellar.Operation.clawback({ from: trustorAddress, asset: bstAsset, amount: '10' }), + ]), + }; + const txParams = { + type: 'clawback', + clawbacks: [{ from: trustorAddress, token: bstTokenName, amount: '200000000' }], + }; + await basecoin + .verifyTransaction({ txParams, txPrebuild, wallet: {}, verification: {} }) + .should.be.rejectedWith(/clawback amount mismatch/); + }); + + it('should throw from mismatch when the tx from address differs', async () => { + const differentFromAddress = 'GBRIS6W5OZNWWFJA6GYRF3JBK5WZNX5WWD2KC6NCOOIEMF7H6JMQLUI4'; + const txPrebuild = { + txBase64: buildTxBase64([ + stellar.Operation.clawback({ from: trustorAddress, asset: bstAsset, amount: '10' }), + ]), + }; + const txParams = { + type: 'clawback', + clawbacks: [{ from: differentFromAddress, token: bstTokenName, amount: '100000000' }], + }; + await basecoin + .verifyTransaction({ txParams, txPrebuild, wallet: {}, verification: {} }) + .should.be.rejectedWith(/clawback 'from' address mismatch/); + }); + + it('should throw token mismatch when the tx asset differs from the requested token', async () => { + const differentIssuerAddress = 'GBRIS6W5OZNWWFJA6GYRF3JBK5WZNX5WWD2KC6NCOOIEMF7H6JMQLUI4'; + const differentTokenName = `txlm:TST-${differentIssuerAddress}`; + const txPrebuild = { + txBase64: buildTxBase64([ + stellar.Operation.clawback({ from: trustorAddress, asset: bstAsset, amount: '10' }), + ]), + }; + const txParams = { + type: 'clawback', + clawbacks: [{ from: trustorAddress, token: differentTokenName, amount: '100000000' }], + }; + await basecoin + .verifyTransaction({ txParams, txPrebuild, wallet: {}, verification: {} }) + .should.be.rejectedWith(/clawback token mismatch/); + }); + + it('should not be treated as clawback when clawbacks is not an array', async () => { + // The type guard requires clawbacks to be an array; a missing or non-array value means the + // params fail the guard and fall through to the default payment-verification path. + const txPrebuild = { + txBase64: buildTxBase64([ + stellar.Operation.clawback({ from: trustorAddress, asset: bstAsset, amount: '10' }), + ]), + }; + const txParams = { type: 'clawback' }; // clawbacks missing + await basecoin + .verifyTransaction({ txParams, txPrebuild, wallet: {}, verification: {} }) + .should.be.rejectedWith(/transaction prebuild does not have any operations/); + }); + }); + }); + + describe('explainTransaction for accountConfig/authorizeTrustline/clawback', () => { + const stellarNetworkPassphrase = stellar.Networks.TESTNET; + const sourceAddress = 'GA34NPQ4M54HHZBKSDZ5B3J3BZHTXKCZD4UFO2OYZERPOASK4DAATSIB'; + const issuerAddress = 'GCWHAO4SVB4KX3Q62QZGZUHUH2GSH3OIV7IS7Y3MPQOFGQFGBP6IYCOU'; + const trustorAddress = 'GCNFRU774FPHLV3HAB6CR54XJYFYITOLU6KS2J5BNCLDPYN7I3DOMIPY'; + const bstAsset = new stellar.Asset('BST', issuerAddress); + const bstTokenName = `txlm:BST-${issuerAddress}`; + + const buildTxBase64 = (stellarOperations: stellar.xdr.Operation[]): string => { + const sourceAccount = new stellar.Account(sourceAddress, '1'); + const transactionBuilder = new stellar.TransactionBuilder(sourceAccount, { + fee: '100', + networkPassphrase: stellarNetworkPassphrase, + }); + stellarOperations.forEach((stellarOperation) => transactionBuilder.addOperation(stellarOperation)); + const builtTransaction = transactionBuilder.setTimeout(0).build(); + return builtTransaction.toEnvelope().toXDR('base64'); + }; + + it('should parse a setOptions op into an accountConfig operation', async () => { + const txBase64 = buildTxBase64([ + stellar.Operation.setOptions({ + setFlags: (stellar.AuthRequiredFlag | stellar.AuthRevocableFlag) as stellar.AuthFlag, + }), + ]); + const explanation = await basecoin.explainTransaction({ txBase64 }); + explanation.operations.length.should.equal(1); + const accountConfigOperation = explanation.operations[0]; + accountConfigOperation.type.should.equal('accountConfig'); + accountConfigOperation.coin.should.equal('txlm'); + // authRequired (1) | authRevocable (2) => setFlags 3; clearFlags is unset for this op. + accountConfigOperation.setFlags.should.equal(3); + should.not.exist(accountConfigOperation.clearFlags); + }); + + it('should parse a setTrustLineFlags op into an authorizeTrustline operation', async () => { + const txBase64 = buildTxBase64([ + stellar.Operation.setTrustLineFlags({ + trustor: trustorAddress, + asset: bstAsset, + flags: { authorized: true }, + }), + ]); + const explanation = await basecoin.explainTransaction({ txBase64 }); + explanation.operations.length.should.equal(1); + const authorizeTrustlineOperation = explanation.operations[0]; + authorizeTrustlineOperation.type.should.equal('authorizeTrustline'); + authorizeTrustlineOperation.coin.should.equal(bstTokenName); + authorizeTrustlineOperation.trustor.should.equal(trustorAddress); + authorizeTrustlineOperation.authorized.should.equal(true); + }); + + it('should parse a clawback op into a clawback operation', async () => { + const txBase64 = buildTxBase64([ + stellar.Operation.clawback({ from: trustorAddress, asset: bstAsset, amount: '10' }), + ]); + const explanation = await basecoin.explainTransaction({ txBase64 }); + explanation.operations.length.should.equal(1); + const clawbackOperation = explanation.operations[0]; + clawbackOperation.type.should.equal('clawback'); + clawbackOperation.coin.should.equal(bstTokenName); + clawbackOperation.from.should.equal(trustorAddress); + // 10 big units clawed back => 10 * 1e7 = 100000000 base units in the explanation. + clawbackOperation.amount.should.equal('100000000'); + }); + }); }); diff --git a/modules/sdk-core/src/bitgo/wallet/BuildParams.ts b/modules/sdk-core/src/bitgo/wallet/BuildParams.ts index a635b2057b..2ea3b10dcb 100644 --- a/modules/sdk-core/src/bitgo/wallet/BuildParams.ts +++ b/modules/sdk-core/src/bitgo/wallet/BuildParams.ts @@ -105,6 +105,10 @@ export const BuildParams = t.exact( sourceChain: t.unknown, destinationChain: t.unknown, trustlines: t.unknown, + flags: t.unknown, + clearFlags: t.unknown, + trustlineAuths: t.unknown, + clawbacks: t.unknown, type: t.unknown, limit: t.unknown, timeBounds: t.unknown,