Skip to content

Commit 2c7db3d

Browse files
authored
Merge pull request #5727 from BitGo/COIN-3288
feat(sdk-coin-stx): added fungible token transfer builder for sip10
2 parents 581ed87 + f17c040 commit 2c7db3d

File tree

10 files changed

+372
-3
lines changed

10 files changed

+372
-3
lines changed

modules/bitgo/src/v2/coinFactory.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ import {
7373
Rbtc,
7474
Sei,
7575
Sgb,
76+
Sip10Token,
7677
Sol,
7778
StellarToken,
7879
Stx,
@@ -374,6 +375,10 @@ function registerCoinConstructors(globalCoinFactory: CoinFactory): void {
374375
AptToken.createTokenConstructors().forEach(({ name, coinConstructor }) =>
375376
globalCoinFactory.register(name, coinConstructor)
376377
);
378+
379+
Sip10Token.createTokenConstructors().forEach(({ name, coinConstructor }) =>
380+
globalCoinFactory.register(name, coinConstructor)
381+
);
377382
}
378383

379384
const GlobalCoinFactory: CoinFactory = new CoinFactory();

modules/bitgo/src/v2/coins/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ import { Rune, Trune } from '@bitgo/sdk-coin-rune';
4646
import { Sei, Tsei } from '@bitgo/sdk-coin-sei';
4747
import { Sgb, Tsgb } from '@bitgo/sdk-coin-sgb';
4848
import { Sol, Tsol } from '@bitgo/sdk-coin-sol';
49-
import { Stx, Tstx } from '@bitgo/sdk-coin-stx';
49+
import { Stx, Tstx, Sip10Token } from '@bitgo/sdk-coin-stx';
5050
import { Sui, Tsui, SuiToken } from '@bitgo/sdk-coin-sui';
5151
import { Tao, Ttao } from '@bitgo/sdk-coin-tao';
5252
import { Tia, Ttia } from '@bitgo/sdk-coin-tia';
@@ -105,7 +105,7 @@ export { Rbtc, Trbtc };
105105
export { Rune, Trune };
106106
export { Sgb, Tsgb };
107107
export { Sol, Tsol };
108-
export { Stx, Tstx };
108+
export { Stx, Tstx, Sip10Token };
109109
export { Sui, Tsui, SuiToken };
110110
export { Tao, Ttao };
111111
export { Tia, Ttia };

modules/bitgo/test/browser/browser.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ describe('Coins', () => {
3838
AptToken: 1,
3939
Icp: 1,
4040
Ticp: 1,
41+
Sip10Token: 1,
4142
};
4243
Object.keys(BitGoJS.Coin)
4344
.filter((coinName) => !excludedKeys[coinName])

modules/sdk-coin-stx/src/lib/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export const FUNCTION_NAME_SENDMANY = 'send-many';
22
export const CONTRACT_NAME_SENDMANY = 'send-many-memo';
33
export const CONTRACT_NAME_STAKING = 'pox-4';
4+
export const FUNCTION_NAME_TRANSFER = 'transfer';
45

56
export const VALID_CONTRACT_FUNCTION_NAMES = [
67
'stack-stx',
@@ -9,6 +10,7 @@ export const VALID_CONTRACT_FUNCTION_NAMES = [
910
'stack-aggregation-commit',
1011
'revoke-delegate-stx',
1112
'send-many',
13+
'transfer',
1214
];
1315

1416
export const DEFAULT_SEED_SIZE_BYTES = 64;
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import { BaseCoin as CoinConfig, NetworkType } from '@bitgo/statics';
2+
import { InvalidParameterValueError, TransactionType } from '@bitgo/sdk-core';
3+
import {
4+
AddressHashMode,
5+
AddressVersion,
6+
ClarityValue,
7+
ContractCallPayload,
8+
FungibleConditionCode,
9+
makeStandardFungiblePostCondition,
10+
PostCondition,
11+
PostConditionMode,
12+
} from '@stacks/transactions';
13+
import BigNum from 'bn.js';
14+
15+
import { AbstractContractBuilder } from './abstractContractBuilder';
16+
import { Transaction } from './transaction';
17+
import {
18+
functionArgsToTokenTransferParams,
19+
getSTXAddressFromPubKeys,
20+
isValidAddress,
21+
isValidContractFunctionName,
22+
} from './utils';
23+
import { TokenTransferParams } from './iface';
24+
import { FUNCTION_NAME_TRANSFER } from './constants';
25+
26+
export class FungibleTokenTransferBuilder extends AbstractContractBuilder {
27+
private _fungibleTokenTransferParams: TokenTransferParams;
28+
private _tokenName: string;
29+
constructor(_coinConfig: Readonly<CoinConfig>) {
30+
super(_coinConfig);
31+
}
32+
33+
initBuilder(tx: Transaction): void {
34+
super.initBuilder(tx);
35+
this._fungibleTokenTransferParams = functionArgsToTokenTransferParams(
36+
(tx.stxTransaction.payload as ContractCallPayload).functionArgs
37+
);
38+
this.contractAddress(this._contractAddress);
39+
this.contractName(this._contractName);
40+
this.functionName(this._functionName);
41+
this.functionArgs(this._functionArgs);
42+
this._postConditionMode = PostConditionMode.Deny;
43+
this._postConditions = this.tokenTransferParamsToPostCondition(this._fungibleTokenTransferParams);
44+
}
45+
46+
/** @inheritdoc */
47+
protected async buildImplementation(): Promise<Transaction> {
48+
await super.buildImplementation();
49+
this.transaction.setTransactionType(TransactionType.Send);
50+
return this.transaction;
51+
}
52+
53+
/**
54+
* Function to check if a transaction is a fungible token contract call
55+
*
56+
* @param {ContractCallPayload} payload
57+
* @returns {Boolean}
58+
*/
59+
public static isFungibleTokenTransferContractCall(payload: ContractCallPayload): boolean {
60+
return FUNCTION_NAME_TRANSFER === payload.functionName.content;
61+
}
62+
63+
/**
64+
* Set the token name
65+
*
66+
* @param {String} tokenName name of the token (@define-fungible-token value)
67+
* @returns {FungibleTokenTransferBuilder} This token transfer builder
68+
*/
69+
tokenName(tokenName: string): this {
70+
this._tokenName = tokenName;
71+
return this;
72+
}
73+
74+
/**
75+
* Validate contract address
76+
*
77+
* @param {String} address contract address
78+
* @returns {FungibleTokenTransferBuilder} This token transfer builder
79+
*/
80+
contractAddress(address: string): this {
81+
if (!isValidAddress(address)) {
82+
throw new InvalidParameterValueError('Invalid address');
83+
}
84+
this._contractAddress = address;
85+
return this;
86+
}
87+
88+
/**
89+
* Validate contract name
90+
*
91+
* @param {String} name contract name
92+
* @returns {FungibleTokenTransferBuilder} This token transfer builder
93+
*/
94+
contractName(name: string): this {
95+
if (name.length === 0) {
96+
throw new InvalidParameterValueError('Invalid name');
97+
}
98+
this._contractName = name;
99+
return this;
100+
}
101+
102+
/**
103+
* Validate function name
104+
*
105+
* @param {String} name function name
106+
* @returns {FungibleTokenTransferBuilder} This token transfer builder
107+
*/
108+
functionName(name: string): this {
109+
if (name.length === 0) {
110+
throw new InvalidParameterValueError('Invalid name');
111+
}
112+
if (!isValidContractFunctionName(name)) {
113+
throw new InvalidParameterValueError(`${name} is not supported contract function name`);
114+
}
115+
this._functionName = name;
116+
return this;
117+
}
118+
119+
/**
120+
* Validate function arguments
121+
*
122+
* @param {ClarityValue[]} args array of clarity value as arguments
123+
* @returns {FungibleTokenTransferBuilder} This token transfer builder
124+
*/
125+
functionArgs(args: ClarityValue[]): this {
126+
if (args.length < 3) {
127+
throw new InvalidParameterValueError('Invalid number of arguments');
128+
}
129+
this._functionArgs = args;
130+
return this;
131+
}
132+
133+
/**
134+
* Function to convert token transfer params to post condition
135+
*
136+
* @param {TokenTransferParams} tokenTransferParams
137+
* @returns {PostCondition[]} returns stx fungible post condition
138+
*/
139+
private tokenTransferParamsToPostCondition(tokenTransferParams: TokenTransferParams): PostCondition[] {
140+
const amount: BigNum = new BigNum(tokenTransferParams.amount);
141+
return [
142+
makeStandardFungiblePostCondition(
143+
getSTXAddressFromPubKeys(
144+
this._fromPubKeys,
145+
this._coinConfig.network.type === NetworkType.MAINNET
146+
? AddressVersion.MainnetMultiSig
147+
: AddressVersion.TestnetMultiSig,
148+
this._fromPubKeys.length > 1 ? AddressHashMode.SerializeP2SH : AddressHashMode.SerializeP2PKH,
149+
this._numberSignatures
150+
).address,
151+
FungibleConditionCode.Equal,
152+
amount,
153+
`${this._contractAddress}.${this._contractName}::${this._tokenName}`
154+
),
155+
];
156+
}
157+
}

modules/sdk-coin-stx/src/lib/iface.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,10 @@ export interface SendParams {
5454
amount: string;
5555
memo?: string;
5656
}
57+
58+
export interface TokenTransferParams {
59+
sender: string;
60+
recipient: string;
61+
amount: string;
62+
memo?: string;
63+
}

modules/sdk-coin-stx/src/lib/transactionBuilderFactory.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { Transaction } from './transaction';
1212
import { ContractBuilder } from './contractBuilder';
1313
import { Utils } from '.';
1414
import { SendmanyBuilder } from './sendmanyBuilder';
15+
import { FungibleTokenTransferBuilder } from './fungibleTokenTransferBuilder';
1516

1617
export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
1718
constructor(_coinConfig: Readonly<CoinConfig>) {
@@ -30,6 +31,9 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
3031
if (SendmanyBuilder.isValidContractCall(this._coinConfig, tx.stxTransaction.payload)) {
3132
return this.getSendmanyBuilder(tx);
3233
}
34+
if (FungibleTokenTransferBuilder.isFungibleTokenTransferContractCall(tx.stxTransaction.payload)) {
35+
return this.getFungibleTokenTransferBuilder(tx);
36+
}
3337
return this.getContractBuilder(tx);
3438
default:
3539
throw new InvalidTransactionError('Invalid transaction');
@@ -67,6 +71,10 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
6771
return TransactionBuilderFactory.initializeBuilder(new SendmanyBuilder(this._coinConfig), tx);
6872
}
6973

74+
getFungibleTokenTransferBuilder(tx?: Transaction): FungibleTokenTransferBuilder {
75+
return TransactionBuilderFactory.initializeBuilder(new FungibleTokenTransferBuilder(this._coinConfig), tx);
76+
}
77+
7078
/**
7179
* Initialize the builder with the given transaction
7280
*

modules/sdk-coin-stx/src/lib/utils.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import {
2929
import { secp256k1 } from '@noble/curves/secp256k1';
3030
import * as _ from 'lodash';
3131
import { InvalidTransactionError, isValidXprv, isValidXpub, SigningError, UtilsError } from '@bitgo/sdk-core';
32-
import { AddressDetails, SendParams } from './iface';
32+
import { AddressDetails, SendParams, TokenTransferParams } from './iface';
3333
import { KeyPair } from '.';
3434
import { StacksNetwork as BitgoStacksNetwork } from '@bitgo/statics';
3535
import { VALID_CONTRACT_FUNCTION_NAMES } from './constants';
@@ -468,6 +468,28 @@ export function functionArgsToSendParams(args: ClarityValue[]): SendParams[] {
468468
});
469469
}
470470

471+
export function functionArgsToTokenTransferParams(args: ClarityValue[]): TokenTransferParams {
472+
if (args.length < 3) {
473+
throw new InvalidTransactionError("function args don't match token transfer declaration");
474+
}
475+
if (
476+
args[0].type !== ClarityType.PrincipalStandard ||
477+
args[1].type !== ClarityType.PrincipalStandard ||
478+
args[2].type !== ClarityType.UInt
479+
) {
480+
throw new InvalidTransactionError("function args don't match token transfer declaration");
481+
}
482+
const tokenTransferParams = {
483+
sender: cvToString(args[0]),
484+
recipient: cvToString(args[1]),
485+
amount: cvToValue(args[2], true),
486+
};
487+
if (args.length === 4 && args[3].type === ClarityType.Buffer) {
488+
tokenTransferParams['memo'] = args[3].buffer.toString('ascii');
489+
}
490+
return tokenTransferParams;
491+
}
492+
471493
/**
472494
* Gets the version of an address
473495
*

modules/sdk-coin-stx/test/unit/resources.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,3 +175,18 @@ export const sendManyRecipients = [
175175
];
176176

177177
export const MEMO = 'memo 1';
178+
179+
export const FUNGIBLE_TOKEN_TRANSFER_CONSTANTS = {
180+
CONTRACT_ADDRESS: 'STAG18E45W613FZ3H4ZMF6QHH426EXM5QTSAVWYH',
181+
CONTRACT_NAME: 'tsip6dp-token',
182+
FUNCTION_NAME: 'transfer',
183+
SENDER_ADDRESS: 'STAG18E45W613FZ3H4ZMF6QHH426EXM5QTSAVWYH',
184+
RECEIVER_ADDRESS: 'SN2NN1JP9AEP5BVE19RNJ6T2MP7NDGRZYST1VDF3M',
185+
TOKEN_NAME: 'tsip6dp-token',
186+
UNSIGNED_SINGLE_SIG_TX:
187+
'80800000000400164247d6f2b425ac5771423ae6c80c754f7172b0000000000000000000000000000000b4000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000030200000000021a1500a1c42f0c11bfe3893f479af18904677685be0d747369703664702d746f6b656e087472616e7366657200000004051a1500a1c42f0c11bfe3893f479af18904677685be0515ab50cac953ac55edc14e2b236854b1ead863fece0100000000000000000000000000002710020000000131',
188+
UNSIGNED_SINGLE_SIG_TX_WITHOUT_MEMO:
189+
'80800000000400164247d6f2b425ac5771423ae6c80c754f7172b0000000000000000000000000000000b4000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000030200000000021a1500a1c42f0c11bfe3893f479af18904677685be0d747369703664702d746f6b656e087472616e7366657200000003051a1500a1c42f0c11bfe3893f479af18904677685be0515ab50cac953ac55edc14e2b236854b1ead863fece0100000000000000000000000000002710',
190+
SIGNED_MULTI_SIG_TX:
191+
'808000000004012fe507c09dbb23c3b7e5d166c81fc4b87692510b000000000000000000000000000000b4000000030200ffa41419c088011baffa87d0113257dbf2033e19ffd5098e9c3e1d8bc606f5e97519688630d57154fcad34967ea04246fe5127203c9971e0b1426cdbd6c132d502004fed6b5699e3211629ad182bf53392374a72e692eb4afe770ffa1fd715661c5706ec16854d14028c49d54806b34b89f108f76d39f2a7675589a94a179bba0ed100038e3c4529395611be9abf6fa3b6987e81d402385e3d605a073f42f407565a4a3d0002030200000000021a1500a1c42f0c11bfe3893f479af18904677685be0d747369703664702d746f6b656e087472616e7366657200000004051a1500a1c42f0c11bfe3893f479af18904677685be0515ab50cac953ac55edc14e2b236854b1ead863fece0100000000000000000000000000002710020000000131',
192+
};

0 commit comments

Comments
 (0)