Skip to content

Commit

Permalink
Extra currency (#81)
Browse files Browse the repository at this point in the history
* Add extra currency support

* EC Sending tests

* Increase test timeout

* Toncenter API getContractState/getState EC support

* TonAPI-v4 getAccount/getAccountLite/getState EC support

* Tonhub client extra currency getAccount test

* TonClient getContractState unit test

* Make ec extracurrency object

---------

Co-authored-by: Trinketer22 <trinketer22@localhost>
  • Loading branch information
Trinketer22 and Trinketer22 authored Jan 31, 2025
1 parent 417ff34 commit d9ccfa0
Show file tree
Hide file tree
Showing 25 changed files with 361 additions and 11 deletions.
1 change: 1 addition & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testPathIgnorePatterns: ["/node_modules/","/dist/"],
testTimeout: 60000,
moduleNameMapper: {
'^axios$': require.resolve('axios'),
}
Expand Down
14 changes: 14 additions & 0 deletions src/client/TonClient.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,20 @@ describeConditional('TonClient', () => {
console.log(info, shardInfo, wcShards);
});

it('should get extra currency info', async () => {
// EC is rolled out only in testned yet
let testClient = new TonClient({
endpoint: 'https://testnet.toncenter.com/api/v2/jsonRPC'
});

let testAddr = Address.parse("0:D36CFC9E0C57F43C1A719CB9F540ED87A694693AE1535B7654B645F52814AFD7");

let res = await testClient.getContractState(testAddr);
let expectedEc =res.extra_currencies.find(e => e.id == 100)!;
expect(expectedEc).not.toBeUndefined();
expect(BigInt(expectedEc.amount)).toBe(10000000n);
});

it('should locate source/result tx', async () => {
let source = Address.parse('UQDDT0TOC4PMp894jtCo3-d1-8ltSjXMX2EuWww_pCNibsUH');
let createdLt = '37508996000002';
Expand Down
16 changes: 15 additions & 1 deletion src/client/TonClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ import {
TupleItem,
TupleReader,
StateInit,
OpenedContract
OpenedContract,
ExtraCurrency
} from '@ton/core';
import { Maybe } from "../utils/maybe";

Expand Down Expand Up @@ -302,6 +303,7 @@ export class TonClient {
let state = info.state as 'frozen' | 'active' | 'uninitialized';
return {
balance,
extra_currencies: info.extra_currencies,
state,
code: info.code !== '' ? Buffer.from(info.code, 'base64') : null,
data: info.data !== '' ? Buffer.from(info.data, 'base64') : null,
Expand Down Expand Up @@ -408,6 +410,8 @@ function createProvider(client: TonClient, address: Address, init: StateInit | n
let state = await client.getContractState(address);
let balance = state.balance;
let last = state.lastTransaction ? { lt: BigInt(state.lastTransaction.lt), hash: Buffer.from(state.lastTransaction.hash, 'base64') } : null;
let ecMap: ExtraCurrency | null = null;

let storage: {
type: 'uninit';
} | {
Expand Down Expand Up @@ -436,8 +440,17 @@ function createProvider(client: TonClient, address: Address, init: StateInit | n
} else {
throw Error('Unsupported state');
}

if(state.extra_currencies.length > 0) {
ecMap = {};
for(let ec of state.extra_currencies) {
ecMap[ec.id] = BigInt(ec.amount);
}
}

return {
balance,
extracurrency: ecMap,
last,
state: storage,
};
Expand Down Expand Up @@ -512,6 +525,7 @@ function createProvider(client: TonClient, address: Address, init: StateInit | n
value,
bounce,
sendMode: message.sendMode,
extracurrency: message.extracurrency,
init: neededInit,
body
});
Expand Down
31 changes: 31 additions & 0 deletions src/client/TonClient4.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { TonClient } from './TonClient';
import { TonClient4 } from './TonClient4';
import { backoff } from '../utils/time';

type ECMap = { [k: number]: bigint };

let describeConditional = process.env.TEST_CLIENTS ? describe : describe.skip;

describeConditional('TonClient', () => {
Expand Down Expand Up @@ -45,6 +47,35 @@ let describeConditional = process.env.TEST_CLIENTS ? describe : describe.skip;
console.log(result);
});

it('should get extra currency info', async () => {
let testAddresses = [
"-1:0000000000000000000000000000000000000000000000000000000000000000",
"0:C4CAC12F5BC7EEF4CF5EC84EE68CCF860921A06CA0395EC558E53E37B13C3B08",
"0:F5FFA780ACEE2A41663C1E32F50D771327275A42FC9D3FAB4F4D9CDE11CCA897"
].map(a => Address.parse(a));

let knownEc = [239, 4294967279];
let expectedEc: ECMap[] = [
{239: 663333333334n, 4294967279: 998444444446n},
{239: 989097920n},
{239: 666666666n, 4294967279: 777777777n}
];

for(let i = 0; i < testAddresses.length; i++) {
let res = await backoff(async () => await client.getAccount(seqno, testAddresses[i]), false);
let resLite = await backoff(async () => await client.getAccountLite(seqno, testAddresses[i]), false);
let expected = expectedEc[i];

for(let testEc of knownEc) {
let expCur = expected[testEc];
if(expCur) {
expect(BigInt(res.account.balance.currencies[testEc])).toEqual(expCur);
expect(BigInt(resLite.account.balance.currencies[testEc])).toEqual(expCur);
}
}
}
});

it('should run method', async () => {
let result = await client.runMethod(seqno, testAddress, 'seqno');
console.log(result);
Expand Down
20 changes: 17 additions & 3 deletions src/client/TonClient4.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/

import axios, { AxiosAdapter, InternalAxiosRequestConfig, AxiosInstance } from "axios";
import { Address, beginCell, Cell, comment, Contract, ContractProvider, ContractState, external, loadTransaction, openContract, OpenedContract, parseTuple, serializeTuple, StateInit, storeMessage, toNano, Transaction, TupleItem, TupleReader } from "@ton/core";
import { Address, beginCell, Cell, comment, Contract, ContractProvider, ContractState, external, ExtraCurrency, loadTransaction, openContract, OpenedContract, parseTuple, serializeTuple, StateInit, storeMessage, toNano, Transaction, TupleItem, TupleReader } from "@ton/core";
import { Maybe } from "../utils/maybe";
import { toUrlSafe } from "../utils/toUrlSafe";
import { z } from 'zod';
Expand Down Expand Up @@ -364,8 +364,19 @@ function createProvider(client: TonClient4, block: number | null, address: Addre
throw Error('Unsupported state');
}

let ecMap: ExtraCurrency | null = null;

if(state.account.balance.currencies) {
ecMap = {};
let currencies = state.account.balance.currencies;
for(let [k, v] of Object.entries(currencies)) {
ecMap[Number(k)] = BigInt(v);
}
}

return {
balance: BigInt(state.account.balance.coins),
extracurrency: ecMap,
last: last,
state: storage
};
Expand Down Expand Up @@ -448,6 +459,7 @@ function createProvider(client: TonClient4, block: number | null, address: Addre
await via.send({
to: address,
value,
extracurrency: message.extracurrency,
bounce,
sendMode: message.sendMode,
init: neededInit,
Expand Down Expand Up @@ -561,7 +573,8 @@ const accountCodec = z.object({
z.object({ type: z.literal('frozen'), stateHash: z.string() })
]),
balance: z.object({
coins: z.string()
coins: z.string(),
currencies: z.record(z.string(), z.string())
}),
last: z.union([
z.null(),
Expand Down Expand Up @@ -589,7 +602,8 @@ const accountLiteCodec = z.object({
z.object({ type: z.literal('frozen'), stateHash: z.string() })
]),
balance: z.object({
coins: z.string()
coins: z.string(),
currencies: z.record(z.string(), z.string())
}),
last: z.union([
z.null(),
Expand Down
5 changes: 5 additions & 0 deletions src/client/api/HttpApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ const blockIdExt = z.object({

const addressInformation = z.object({
balance: z.union([z.number(), z.string()]),
extra_currencies: z.array(z.object({
'@type': z.literal("extraCurrency"),
id: z.number(),
amount: z.string()
})),
state: z.union([z.literal('active'), z.literal('uninitialized'), z.literal('frozen')]),
data: z.string(),
code: z.string(),
Expand Down
28 changes: 28 additions & 0 deletions src/utils/testWallets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { OpenedContract } from '@ton/core';
import { WalletContractV5R1 } from '../wallets/v5r1/WalletContractV5R1';
import { WalletContractV5Beta } from '../wallets/v5beta/WalletContractV5Beta';
import { WalletContractV4 } from '../wallets/WalletContractV4';
import { WalletContractV3R2 } from '../wallets/WalletContractV3R2';
import { WalletContractV3R1 } from '../wallets/WalletContractV3R1';
import { WalletContractV2R2 } from '../wallets/WalletContractV2R2';
import { WalletContractV2R1 } from '../wallets/WalletContractV2R1';
import { WalletContractV1R2 } from '../wallets/WalletContractV1R2';
import { WalletContractV1R1 } from '../wallets/WalletContractV1R1';


type WalletContract = WalletContractV5R1 | WalletContractV5Beta | WalletContractV4 | WalletContractV3R2 | WalletContractV3R1 | WalletContractV2R2 | WalletContractV2R1 | WalletContractV1R2 | WalletContractV1R1;

export const tillNextSeqno = async(wallet: OpenedContract<WalletContract>, oldSeqno: number, maxTries: number = 10) => {
let seqNoAfter = oldSeqno;
let tried = 0;

do {
await new Promise((resolve, reject) => {
setTimeout(resolve, 2000);
});
seqNoAfter = await wallet.getSeqno();
if(tried++ > maxTries) {
throw Error("To many retries, transaction likely failed!");
}
} while(seqNoAfter == oldSeqno);
}
28 changes: 27 additions & 1 deletion src/wallets/WalletContractV1R1.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { randomTestKey } from "../utils/randomTestKey";
import { createTestClient4 } from "../utils/createTestClient4";
import { Address, internal } from "@ton/core";
import { WalletContractV1R1 } from "./WalletContractV1R1";
import { tillNextSeqno } from "../utils/testWallets";

describe('WalletContractV1R1', () => {

Expand Down Expand Up @@ -46,5 +47,30 @@ describe('WalletContractV1R1', () => {

// Perform transfer
await contract.send(transfer);
await tillNextSeqno(contract, seqno);
});
});

it('should perform extra currency transfer', async () => {
// Create contract
let client = createTestClient4();
let key = randomTestKey('v4-treasure');
let contract = client.open(WalletContractV1R1.create({ workchain: 0, publicKey: key.publicKey }));

// Prepare transfer
let seqno = await contract.getSeqno();
let transfer = contract.createTransfer({
seqno,
secretKey: key.secretKey,
message: internal({
to: 'kQD6oPnzaaAMRW24R8F0_nlSsJQni0cGHntR027eT9_sgtwt',
value: '0.01',
extracurrency: {100: BigInt(10 ** 6)},
body: 'Hello, extra currency v1r1!'
})
});

// Perform transfer
await contract.send(transfer);
await tillNextSeqno(contract, seqno);
});
});
1 change: 1 addition & 0 deletions src/wallets/WalletContractV1R1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ export class WalletContractV1R1 implements Contract {
message: internal({
to: args.to,
value: args.value,
extracurrency: args.extracurrency,
init: args.init,
body: args.body,
bounce: args.bounce
Expand Down
28 changes: 27 additions & 1 deletion src/wallets/WalletContractV1R2.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { randomTestKey } from "../utils/randomTestKey";
import { createTestClient4 } from "../utils/createTestClient4";
import { Address, internal } from "@ton/core";
import { WalletContractV1R2 } from "./WalletContractV1R2";
import { tillNextSeqno } from "../utils/testWallets";

describe('WalletContractV1R2', () => {
it('should has balance and correct address', async () => {
Expand Down Expand Up @@ -44,5 +45,30 @@ describe('WalletContractV1R2', () => {

// Perform transfer
await contract.send(transfer);
await tillNextSeqno(contract, seqno);
});
});

it('should perform extra currency transfer', async () => {
// Create contract
let client = createTestClient4();
let key = randomTestKey('v4-treasure');
let contract = client.open(WalletContractV1R2.create({ workchain: 0, publicKey: key.publicKey }));

// Prepare transfer
let seqno = await contract.getSeqno();
let transfer = contract.createTransfer({
seqno,
secretKey: key.secretKey,
message: internal({
to: 'kQD6oPnzaaAMRW24R8F0_nlSsJQni0cGHntR027eT9_sgtwt',
value: '0.01',
extracurrency: {100: BigInt(10 ** 6)},
body: 'Hello, extra currency v1r2!'
})
});

// Perform transfer
await contract.send(transfer);
await tillNextSeqno(contract, seqno);
});
});
1 change: 1 addition & 0 deletions src/wallets/WalletContractV1R2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ export class WalletContractV1R2 implements Contract {
message: internal({
to: args.to,
value: args.value,
extracurrency: args.extracurrency,
init: args.init,
body: args.body,
bounce: args.bounce
Expand Down
28 changes: 27 additions & 1 deletion src/wallets/WalletContractV2R1.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { randomTestKey } from "../utils/randomTestKey";
import { createTestClient4 } from "../utils/createTestClient4";
import { Address, internal } from "@ton/core";
import { WalletContractV2R1 } from "./WalletContractV2R1";
import { tillNextSeqno } from "../utils/testWallets";

describe('WalletContractV2R1', () => {
it('should has balance and correct address', async () => {
Expand Down Expand Up @@ -44,5 +45,30 @@ describe('WalletContractV2R1', () => {

// Perform transfer
await contract.send(transfer);
await tillNextSeqno(contract, seqno);
});
});

it('should perform extra currency transfer', async () => {
// Create contract
let client = createTestClient4();
let key = randomTestKey('v4-treasure');
let contract = client.open(WalletContractV2R1.create({ workchain: 0, publicKey: key.publicKey }));

// Prepare transfer
let seqno = await contract.getSeqno();
let transfer = contract.createTransfer({
seqno,
secretKey: key.secretKey,
messages: [internal({
to: 'kQD6oPnzaaAMRW24R8F0_nlSsJQni0cGHntR027eT9_sgtwt',
value: '0.01',
extracurrency: {100: BigInt(10 ** 6)},
body: 'Hello, extra currency v2r1!'
})]
});

// Perform transfer
await contract.send(transfer);
await tillNextSeqno(contract, seqno);
});
});
1 change: 1 addition & 0 deletions src/wallets/WalletContractV2R1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ export class WalletContractV2R1 implements Contract {
messages: [internal({
to: args.to,
value: args.value,
extracurrency: args.extracurrency,
init: args.init,
body: args.body,
bounce: args.bounce
Expand Down
Loading

0 comments on commit d9ccfa0

Please sign in to comment.