Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 14 additions & 10 deletions modules/sdk-coin-canton/src/canton.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,19 +118,23 @@ export class Canton extends BaseCoin {
case TransactionType.Send:
if (txParams.recipients !== undefined) {
const filteredRecipients = txParams.recipients?.map((recipient) => {
const { address, amount } = recipient;
const { address, amount, tokenName } = recipient;
const [addressPart, memoId] = address.split('?memoId=');
if (memoId) {
return { address: addressPart, amount, memo: memoId };
}
return { address, amount };
return {
address: addressPart,
amount,
...(memoId && { memo: memoId }),
...(tokenName && { tokenName }),
};
});
const filteredOutputs = explainedTx.outputs?.map((output) => {
const { address, amount, memo } = output;
if (memo) {
return { address, amount, memo };
}
return { address, amount };
const { address, amount, tokenName, memo } = output;
return {
address,
amount,
...(memo && { memo }),
...(tokenName && { tokenName }),
};
});
if (JSON.stringify(filteredRecipients) !== JSON.stringify(filteredOutputs)) {
throw new Error('Tx outputs do not match with expected txParams recipients');
Expand Down
3 changes: 3 additions & 0 deletions modules/sdk-coin-canton/src/lib/iface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,15 @@ export interface TxData {
amount: string;
acknowledgeData?: TransferAcknowledge;
memoId?: string;
token?: string;
}

export interface PreparedTxnParsedInfo {
sender: string;
receiver: string;
amount: string;
memoId?: string;
token?: string;
}

export interface WalletInitTxData {
Expand Down Expand Up @@ -152,4 +154,5 @@ export interface CantonTransferRequest {
expiryEpoch: number;
sendViaOneStep: boolean;
memoId?: string;
token?: string;
}
10 changes: 8 additions & 2 deletions modules/sdk-coin-canton/src/lib/transaction/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,9 @@ export class Transaction extends BaseTransaction {
if (parsedInfo.memoId) {
result.memoId = parsedInfo.memoId;
}
if (parsedInfo.token) {
result.token = parsedInfo.token;
}
return result;
}

Expand Down Expand Up @@ -208,12 +211,12 @@ export class Transaction extends BaseTransaction {
const input: Entry = {
address: txData.sender,
value: txData.amount,
coin: this._coinConfig.name,
coin: txData.token ? txData.token : this._coinConfig.name,
};
const output: Entry = {
address: txData.receiver,
value: txData.amount,
coin: this._coinConfig.name,
coin: txData.token ? txData.token : this._coinConfig.name,
};
inputs.push(input);
outputs.push(output);
Expand Down Expand Up @@ -254,6 +257,9 @@ export class Transaction extends BaseTransaction {
if (txData.memoId) {
output.memo = txData.memoId;
}
if (txData.token) {
output.tokenName = txData.token;
}
outputs.push(output);
outputAmount = txData.amount;
break;
Expand Down
18 changes: 18 additions & 0 deletions modules/sdk-coin-canton/src/lib/transferBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export class TransferBuilder extends TransactionBuilder {
private _sendOneStep = false;
private _expiryEpoch: number;
private _memoId: string;
private _token: string;
constructor(_coinConfig: Readonly<CoinConfig>) {
super(_coinConfig);
}
Expand Down Expand Up @@ -145,6 +146,20 @@ export class TransferBuilder extends TransactionBuilder {
return this;
}

/**
* Sets the optional token field if present, used for canton token transaction
* @param name - the bitgo name of the token
* @returns The current builder for chaining
* @throws Error if name is invalid
*/
token(name: string): this {
if (!name || !name.trim()) {
throw new Error('token name must be a non-empty string');
}
this._token = name.trim();
return this;
}

/**
* Get the canton transfer request object
* @returns CantonTransferRequest
Expand All @@ -163,6 +178,9 @@ export class TransferBuilder extends TransactionBuilder {
if (this._memoId) {
data.memoId = this._memoId;
}
if (this._token) {
data.token = this._token;
}
return data;
}

Expand Down
38 changes: 38 additions & 0 deletions modules/sdk-coin-canton/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import BigNumber from 'bignumber.js';
import crypto from 'crypto';

import { BaseUtils, isValidEd25519PublicKey, TransactionType } from '@bitgo/sdk-core';
import { coins, CantonToken } from '@bitgo/statics';

import { computePreparedTransaction } from '../../resources/hash/hash.js';
import { PreparedTransaction } from '../../resources/proto/preparedTransaction.js';
Expand Down Expand Up @@ -93,6 +94,9 @@ export class Utils implements BaseUtils {
let receiver = '';
let amount = '';
let memoId: string | undefined;
let instrumentId: string | undefined;
let instrumentAdmin: string | undefined;
let token: string | undefined;
let preApprovalNode: RecordField[] = [];
let transferNode: RecordField[] = [];
let transferAcceptRejectNode: RecordField[] = [];
Expand Down Expand Up @@ -165,6 +169,21 @@ export class Utils implements BaseUtils {
const amountData = getField(transferRecord, 'amount');
if (amountData?.oneofKind === 'numeric') amount = amountData.numeric ?? '';

const instrumentField = getField(transferRecord, 'instrumentId');
if (instrumentField?.oneofKind === 'record') {
const instrumentFields = instrumentField.record?.fields ?? [];

const adminData = getField(instrumentFields, 'admin');
if (adminData?.oneofKind === 'party') {
instrumentAdmin = adminData.party ?? '';
}

const idData = getField(instrumentFields, 'id');
if (idData?.oneofKind === 'text') {
instrumentId = idData.text ?? '';
}
}

const metaField = getField(transferRecord, 'meta');
if (metaField?.oneofKind === 'record') {
const metaFields = metaField.record?.fields;
Expand Down Expand Up @@ -214,6 +233,10 @@ export class Utils implements BaseUtils {
if (memoId) {
parsedData.memoId = memoId;
}
if (instrumentId && instrumentAdmin) {
token = this.findTokenNameByContractAddress(`${instrumentAdmin}:${instrumentId}`);
parsedData.token = token;
}
return parsedData;
}

Expand Down Expand Up @@ -371,6 +394,21 @@ export class Utils implements BaseUtils {
private convertAmountToLowestUnit(value: BigNumber): string {
return value.multipliedBy(new BigNumber(10).pow(10)).toFixed(0);
}

/**
* Get the bitgo token name using the on-chain instrument details
* @param contractAddress - the contract address of the form, `instrumentAdmin:instrumentId`
* @returns tokenName if contractAddress matches with any supported canton tokens
*/
private findTokenNameByContractAddress(contractAddress: string): string | undefined {
if (contractAddress.includes('Amulet')) {
return undefined;
}
const cantonToken = coins
.filter((coin) => coin instanceof CantonToken && coin.contractAddress === contractAddress)
.map((coin) => coin as CantonToken);
return cantonToken ? cantonToken[0].name : undefined;
}
}

const utils = new Utils();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { BitGoAPI } from '@bitgo/sdk-api';
import { ITransactionRecipient, Wallet } from '@bitgo/sdk-core';
import { TestBitGo, TestBitGoAPI } from '@bitgo/sdk-test';

import { CantonTokenTransferRawTxn, TokenTxParams } from '../resources';
import { CantonToken } from '../../src';

describe('Canton Token integration tests', function () {
const tokenName = 'tcanton:testtoken';
let bitgo: TestBitGoAPI;
let basecoin: CantonToken;
let newTxPrebuild: () => { txHex: string; txInfo: Record<string, unknown> };
let newTxParams: () => { recipients: ITransactionRecipient[] };
let wallet: Wallet;
const txPrebuild = {
txHex: CantonTokenTransferRawTxn,
txInfo: {},
};
const txParams = {
recipients: [
{
address: TokenTxParams.RECIPIENT_ADDRESS,
amount: TokenTxParams.AMOUNT,
tokenName: TokenTxParams.TOKEN,
},
],
};
before(() => {
bitgo = TestBitGo.decorate(BitGoAPI, { env: 'mock' });
CantonToken.createTokenConstructors().forEach(({ name, coinConstructor }) => {
bitgo.safeRegister(name, coinConstructor);
});
basecoin = bitgo.coin(tokenName) as CantonToken;
newTxPrebuild = () => {
return structuredClone(txPrebuild);
};
newTxParams = () => {
return structuredClone(txParams);
};
wallet = new Wallet(bitgo, basecoin, {});
});

describe('Verify Transaction', function () {
it('should verify token transfer transaction', async function () {
const txPrebuild = newTxPrebuild();
const txParams = newTxParams();
const isTxnVerifies = await basecoin.verifyTransaction({ txPrebuild: txPrebuild, txParams: txParams, wallet });
isTxnVerifies.should.equal(true);
});
});
});
23 changes: 23 additions & 0 deletions modules/sdk-coin-canton/test/resources.ts

Large diffs are not rendered by default.

Loading