Skip to content

feat(sdk-coin-ton): add jetton skeleton #6618

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
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
9 changes: 9 additions & 0 deletions modules/bitgo/src/v2/coinFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { AlgoToken } from '@bitgo/sdk-coin-algo';
import { Bcha, Tbcha } from '@bitgo/sdk-coin-bcha';
import { HbarToken } from '@bitgo/sdk-coin-hbar';
import { Near, TNear, Nep141Token } from '@bitgo/sdk-coin-near';
import { TonToken } from '@bitgo/sdk-coin-ton';
import { SolToken } from '@bitgo/sdk-coin-sol';
import { TrxToken } from '@bitgo/sdk-coin-trx';
import { CoinFactory, CoinConstructor } from '@bitgo/sdk-core';
Expand Down Expand Up @@ -36,6 +37,7 @@ import {
VetTokenConfig,
TaoTokenConfig,
PolyxTokenConfig,
TonTokenConfig,
} from '@bitgo/statics';
import {
Ada,
Expand Down Expand Up @@ -532,6 +534,10 @@ export function registerCoinConstructors(coinFactory: CoinFactory, coinMap: Coin
VetToken.createTokenConstructors().forEach(({ name, coinConstructor }) =>
coinFactory.register(name, coinConstructor)
);

TonToken.createTokenConstructors([...tokens.bitcoin.ton.tokens, ...tokens.testnet.ton.tokens]).forEach(
({ name, coinConstructor }) => coinFactory.register(name, coinConstructor)
);
}

export function getCoinConstructor(coinName: string): CoinConstructor | undefined {
Expand Down Expand Up @@ -983,6 +989,9 @@ export function getTokenConstructor(tokenConfig: TokenConfig): CoinConstructor |
case 'zeta':
case 'tzeta':
return CosmosToken.createTokenConstructor(tokenConfig as CosmosTokenConfig);
case 'ton':
case 'tton':
return TonToken.createTokenConstructor(tokenConfig as TonTokenConfig);
default:
return undefined;
}
Expand Down
1 change: 1 addition & 0 deletions modules/sdk-coin-ton/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './lib';
export * from './register';
export * from './ton';
export * from './tton';
export * from './tonToken';
7 changes: 7 additions & 0 deletions modules/sdk-coin-ton/src/register.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import { BitGoBase } from '@bitgo/sdk-core';
import { Ton } from './ton';
import { Tton } from './tton';
import { TonToken } from './tonToken';

export const register = (sdk: BitGoBase): void => {
sdk.register('ton', Ton.createInstance);
sdk.register('tton', Tton.createInstance);

// Register Jetton tokens
const tokens = TonToken.createTokenConstructors();
tokens.forEach((token) => {
sdk.register(token.name, token.coinConstructor);
});
};
105 changes: 105 additions & 0 deletions modules/sdk-coin-ton/src/tonToken.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { BitGoBase, CoinConstructor, NamedCoinConstructor, VerifyTransactionOptions } from '@bitgo/sdk-core';
import BigNumber from 'bignumber.js';
import { coins, TonTokenConfig, NetworkType, tokens } from '@bitgo/statics';

import { Transaction } from './lib';
import { Ton } from './ton';

export class TonToken extends Ton {
public readonly tokenConfig: TonTokenConfig;

constructor(bitgo: BitGoBase, tokenConfig: TonTokenConfig) {
const staticsCoin = tokenConfig.network === NetworkType.MAINNET ? coins.get('ton') : coins.get('tton');
super(bitgo, staticsCoin);
this.tokenConfig = tokenConfig;
}

static createTokenConstructor(config: TonTokenConfig): CoinConstructor {
return (bitgo: BitGoBase) => new TonToken(bitgo, config);
}

static createTokenConstructors(
tokenConfig: TonTokenConfig[] = [...tokens.bitcoin.ton.tokens, ...tokens.testnet.ton.tokens]
): NamedCoinConstructor[] {
const tokensCtors: NamedCoinConstructor[] = [];
for (const token of tokenConfig) {
const tokenConstructor = TonToken.createTokenConstructor(token);
tokensCtors.push({ name: token.type, coinConstructor: tokenConstructor });
}
return tokensCtors;
}

get name(): string {
return this.tokenConfig.name;
}

get coin(): string {
return this.tokenConfig.coin;
}

get jettonMaster(): string {
return this.tokenConfig.jettonMaster;
}

get decimalPlaces(): number {
return this.tokenConfig.decimalPlaces;
}

getChain(): string {
return this.tokenConfig.type;
}

getBaseChain(): string {
return this.coin;
}

getFullName(): string {
return 'TON Token';
}

getBaseFactor(): number {
return Math.pow(10, this.tokenConfig.decimalPlaces);
}

async verifyTransaction(params: VerifyTransactionOptions): Promise<boolean> {
const { txPrebuild: txPrebuild, txParams: txParams } = params;
const rawTx = txPrebuild.txHex;
let totalAmount = new BigNumber(0);
if (!rawTx) {
throw new Error('missing required tx prebuild property txHex');
}
const coinConfig = coins.get(this.getChain());
const transaction = new Transaction(coinConfig);
transaction.fromRawTransaction(Buffer.from(rawTx, 'hex').toString('base64'));
const explainedTx = transaction.explainTransaction();
if (txParams.recipients !== undefined) {
txParams.recipients.forEach((recipient) => {
if (recipient.tokenName && recipient.tokenName !== coinConfig.name) {
throw new Error('incorrect token name specified in recipients');
}
recipient.tokenName = coinConfig.name;
});
const filteredRecipients = txParams.recipients?.map((recipient) => ({
address: recipient.address,
amount: recipient.amount,
tokenName: recipient.tokenName,
}));
const filteredOutputs = explainedTx.outputs.map((output) => ({
address: output.address,
amount: output.amount,
tokenName: output.tokenName,
}));
const outputsMatch = JSON.stringify(filteredRecipients) === JSON.stringify(filteredOutputs);
if (!outputsMatch) {
throw new Error('Tx outputs does not match with expected txParams recipients');
}
for (const recipient of txParams.recipients) {
totalAmount = totalAmount.plus(recipient.amount);
}
if (!totalAmount.isEqualTo(explainedTx.outputAmount)) {
throw new Error('Tx total amount does not match with expected total amount field');
}
}
return true;
}
}
105 changes: 105 additions & 0 deletions modules/statics/src/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,10 @@ export interface Nep141TokenConstructorOptions extends AccountConstructorOptions
storageDepositAmount: string;
}

export interface TonTokenConstructorOptions extends AccountConstructorOptions {
jettonMaster: string;
}

export interface VetTokenConstructorOptions extends AccountConstructorOptions {
contractAddress: string;
gasTankToken?: string;
Expand Down Expand Up @@ -635,6 +639,18 @@ export class Nep141Token extends AccountCoinToken {
}
}

export class TonToken extends AccountCoinToken {
public jettonMaster: string;

constructor(options: TonTokenConstructorOptions) {
super({
...options,
});

this.jettonMaster = options.jettonMaster;
}
}

export class VetToken extends AccountCoinToken {
public contractAddress: string;
public gasTankToken?: string;
Expand Down Expand Up @@ -3091,6 +3107,95 @@ export function sip10Token(
* @param network? Optional token network. Defaults to the testnet Stacks network.
* @param features? Features of this coin. Defaults to the DEFAULT_FEATURES defined in `AccountCoin`
*/
/**
* Factory function for TON token instances.
*
* @param id uuid v4
* @param name unique identifier of the token
* @param fullName Complete human-readable name of the token
* @param decimalPlaces Number of decimal places this token supports (divisibility exponent)
* @param jettonMaster Jetton master address of this token
* @param asset Asset which this coin represents. This is the same for both mainnet and testnet variants of a coin.
* @param features Features of this coin. Defaults to the DEFAULT_FEATURES defined in `AccountCoin`
* @param prefix Optional token prefix. Defaults to empty string
* @param suffix Optional token suffix. Defaults to token name.
* @param network Optional token network. Defaults to TON main network.
* @param primaryKeyCurve The elliptic curve for this chain/token
*/
export function tonToken(
id: string,
name: string,
fullName: string,
decimalPlaces: number,
jettonMaster: string,
asset: UnderlyingAsset,
features: CoinFeature[] = AccountCoin.DEFAULT_FEATURES,
prefix = '',
suffix: string = name.toUpperCase(),
network: AccountNetwork = Networks.main.ton,
primaryKeyCurve: KeyCurve = KeyCurve.Ed25519
) {
return Object.freeze(
new TonToken({
id,
name,
fullName,
network,
jettonMaster,
prefix,
suffix,
features,
decimalPlaces,
asset,
isToken: true,
primaryKeyCurve,
baseUnit: BaseUnit.TON,
})
);
}

/**
* Factory function for testnet TON token instances.
*
* @param id uuid v4
* @param name unique identifier of the token
* @param fullName Complete human-readable name of the token
* @param decimalPlaces Number of decimal places this token supports (divisibility exponent)
* @param jettonMaster Jetton master address of this token
* @param asset Asset which this coin represents. This is the same for both mainnet and testnet variants of a coin.
* @param features Features of this coin. Defaults to the DEFAULT_FEATURES defined in `AccountCoin`
* @param prefix Optional token prefix. Defaults to empty string
* @param suffix Optional token suffix. Defaults to token name.
* @param network Optional token network. Defaults to the testnet TON network.
*/
export function ttonToken(
id: string,
name: string,
fullName: string,
decimalPlaces: number,
jettonMaster: string,
asset: UnderlyingAsset,
features: CoinFeature[] = AccountCoin.DEFAULT_FEATURES,
prefix = '',
suffix: string = name.toUpperCase(),
network: AccountNetwork = Networks.test.ton,
primaryKeyCurve: KeyCurve = KeyCurve.Ed25519
) {
return tonToken(
id,
name,
fullName,
decimalPlaces,
jettonMaster,
asset,
features,
prefix,
suffix,
network,
primaryKeyCurve
);
}

export function tsip10Token(
id: string,
name: string,
Expand Down
41 changes: 40 additions & 1 deletion modules/statics/src/tokenConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
Erc20Coin,
Erc721Coin,
HederaToken,
TonToken,
Nep141Token,
OpethERC20Token,
PolygonERC20Token,
Expand Down Expand Up @@ -126,6 +127,10 @@ export type Nep141TokenConfig = BaseNetworkConfig & {
storageDepositAmount: string;
};

export type TonTokenConfig = BaseNetworkConfig & {
jettonMaster: string;
};

export type VetTokenConfig = BaseNetworkConfig & {
contractAddress: string;
};
Expand Down Expand Up @@ -156,7 +161,8 @@ export type TokenConfig =
| CosmosTokenConfig
| VetTokenConfig
| TaoTokenConfig
| PolyxTokenConfig;
| PolyxTokenConfig
| TonTokenConfig;

export interface Tokens {
bitcoin: {
Expand Down Expand Up @@ -249,6 +255,9 @@ export interface Tokens {
cosmos: {
tokens: CosmosTokenConfig[];
};
ton: {
tokens: TonTokenConfig[];
};
};
testnet: {
eth: {
Expand Down Expand Up @@ -340,6 +349,9 @@ export interface Tokens {
cosmos: {
tokens: CosmosTokenConfig[];
};
ton: {
tokens: TonTokenConfig[];
};
};
}

Expand Down Expand Up @@ -995,6 +1007,25 @@ function getCosmosTokenConfig(coin: CosmosChainToken): CosmosTokenConfig {
};
}

function getTonTokenConfig(coin: TonToken): TonTokenConfig {
return {
type: coin.name,
coin: coin.network.type === NetworkType.MAINNET ? 'ton' : 'tton',
network: coin.network.type === NetworkType.MAINNET ? 'Mainnet' : 'Testnet',
name: coin.fullName,
jettonMaster: coin.jettonMaster,
decimalPlaces: coin.decimalPlaces,
};
}

const getFormattedTonTokens = (customCoinMap = coins) =>
customCoinMap.reduce((acc: TonTokenConfig[], coin) => {
if (coin instanceof TonToken) {
acc.push(getTonTokenConfig(coin));
}
return acc;
}, []);

export const getFormattedTokens = (coinMap = coins): Tokens => {
const formattedAptNFTCollections = getFormattedAptNFTCollections(coinMap);
return {
Expand Down Expand Up @@ -1092,6 +1123,9 @@ export const getFormattedTokens = (coinMap = coins): Tokens => {
cosmos: {
tokens: getFormattedCosmosChainTokens(coinMap).filter((token) => token.network === 'Mainnet'),
},
ton: {
tokens: getFormattedTonTokens(coinMap).filter((token) => token.network === 'Mainnet'),
},
},
testnet: {
eth: {
Expand Down Expand Up @@ -1187,6 +1221,9 @@ export const getFormattedTokens = (coinMap = coins): Tokens => {
cosmos: {
tokens: getFormattedCosmosChainTokens(coinMap).filter((token) => token.network === 'Testnet'),
},
ton: {
tokens: getFormattedTonTokens(coinMap).filter((token) => token.network === 'Testnet'),
},
},
};
};
Expand Down Expand Up @@ -1292,6 +1329,8 @@ export function getFormattedTokenConfigForCoin(coin: Readonly<BaseCoin>): TokenC
return getCosmosTokenConfig(coin);
} else if (coin instanceof VetToken) {
return getVetTokenConfig(coin);
} else if (coin instanceof TonToken) {
return getTonTokenConfig(coin);
}
return undefined;
}