Skip to content

Commit

Permalink
Merge branch 'master' into ton-org-master
Browse files Browse the repository at this point in the history
  • Loading branch information
dvlkv committed Oct 24, 2023
2 parents dba6839 + bd3f556 commit ea77703
Show file tree
Hide file tree
Showing 15 changed files with 1,405 additions and 42 deletions.
13 changes: 12 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## Added
- `TonClient4.getAccountTransactionsParsed` method (thanks @vzhovnitsky)
- `WalletV5` contract (thanks @siandreev)
- blockchain fees estimation via `computeStorageFees`/`computeExternalMessageFees`/`computeGasPrices`/`computeMessageForwardFees` (thanks @vzhovnitsky)

## Fixed
- Uri encode get method name for `TonClient4` (thanks @krigga)
- Improved `parseStackItem` due to toncenter.com bug


## [13.7.0] - 2023-09-18

## Added
Expand All @@ -12,7 +24,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Fixed
- Uri encode get method name for `TonClient4`


## [13.6.1] - 2023-08-24

## Fixed
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
},
"devDependencies": {
"@release-it/keep-a-changelog": "^3.1.0",
"@ton/core": "^0.52.2",
"@ton/core": "^0.53.0",
"@ton/crypto": "3.2.0",
"@ton/emulator": "^2.1.1",
"@types/jest": "^27.0.1",
Expand Down Expand Up @@ -47,7 +47,7 @@
"zod": "^3.21.4"
},
"peerDependencies": {
"@ton/core": ">=0.51.0",
"@ton/core": ">=0.53.0",
"@ton/crypto": ">=3.2.0"
},
"publishConfig": {
Expand Down
64 changes: 46 additions & 18 deletions src/client/TonClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,28 +295,56 @@ export class TonClient {
}
}

function parseStackEntry(s: any): TupleItem {
switch (s["@type"]) {
case "tvm.stackEntryNumber":
return { type: 'int', value: BigInt(s.number.number) };
case "tvm.stackEntryCell":
return { type: 'cell', cell: Cell.fromBase64(s.cell) };
case 'tvm.stackEntryTuple':
return { type: 'tuple', items: s.tuple.elements.map(parseStackEntry) };
default:
throw Error("Unsupported item type: " + s["@type"]);
}
}

function parseStackItem(s: any): TupleItem {
if (s[0] === 'num') {
let val = s[1] as string;
if (val.startsWith('-')) {
return { type: 'int', value: -BigInt(val.slice(1)) };
} else {
return { type: 'int', value: BigInt(val) };
}
} else if (s[0] === 'null') {
return { type: 'null' };
} else if (s[0] === 'cell') {
return { type: 'cell', cell: Cell.fromBoc(Buffer.from(s[1].bytes, 'base64'))[0] };
} else if (s[0] === 'slice') {
return { type: 'slice', cell: Cell.fromBoc(Buffer.from(s[1].bytes, 'base64'))[0] };
} else if (s[0] === 'builder') {
return { type: 'builder', cell: Cell.fromBoc(Buffer.from(s[1].bytes, 'base64'))[0] };
} else if (s[0] === 'tuple' || s[0] === 'list') {
// toncenter.com missbehaviour
if (s[1].elements.length === 0) {
return { type: 'null' };
}
return {
type: s[0],
items: s[1].elements.map(parseStackEntry)
};
} else {
throw Error('Unsupported stack item type: ' + s[0])
}
}

function parseStack(src: any[]) {
let stack: TupleItem[] = [];

for (let s of src) {
if (s[0] === 'num') {
let val = s[1] as string;
if (val.startsWith('-')) {
stack.push({ type: 'int', value: -BigInt(val.slice(1)) });
} else {
stack.push({ type: 'int', value: BigInt(val) });
}
} else if (s[0] === 'null') {
stack.push({ type: 'null' });
} else if (s[0] === 'cell') {
stack.push({ type: 'cell', cell: Cell.fromBoc(Buffer.from(s[1].bytes, 'base64'))[0] });
} else if (s[0] === 'slice') {
stack.push({ type: 'slice', cell: Cell.fromBoc(Buffer.from(s[1].bytes, 'base64'))[0] });
} else if (s[0] === 'builder') {
stack.push({ type: 'builder', cell: Cell.fromBoc(Buffer.from(s[1].bytes, 'base64'))[0] });
} else {
throw Error('Unsupported stack item type: ' + s[0])
}
stack.push(parseStackItem(s));
}

return new TupleReader(stack);
}

Expand Down
12 changes: 10 additions & 2 deletions src/client/TonClient4.spec.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { Address, beginCell } from '@ton/core';
import { TonClient } from './TonClient';
import { TonClient4 } from './TonClient4';
import { backoff } from '../utils/time';

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

describeConditional('TonClient', () => {
describeConditional('TonClient', () => {
let client = new TonClient4({
endpoint: 'https://mainnet-v4.tonhubapi.com',
});
const testAddress = Address.parse('EQCD39VS5jcptHL8vMjEXrzGaRcCVYto7HUn4bpAOg8xqB2N');
const testAddress = Address.parse('EQBicYUqh1j9Lnqv9ZhECm0XNPaB7_HcwoBb3AJnYYfqB38_');

let seqno!: number;
beforeAll(async () => {
Expand All @@ -27,6 +28,13 @@ describeConditional('TonClient', () => {
console.log(account, accountLite);
});

it('should get account parsed transactions', async () => {
let accountLite = await backoff(async () => await client.getAccountLite(seqno, testAddress), true);
let parsedTransactions = await backoff(async () => await client.getAccountTransactionsParsed(testAddress, BigInt(accountLite.account.last!.lt), Buffer.from(accountLite.account.last!.hash, 'base64'), 10), true);

console.log(parsedTransactions.transactions.length);
}, 60_000);

it('should get config', async () => {
let config = await client.getConfig(seqno);
console.log(config);
Expand Down
169 changes: 162 additions & 7 deletions src/client/TonClient4.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,34 @@ export class TonClient4 {
return tx;
}

/**
* Load parsed account transactions
* @param address address
* @param lt last transaction lt
* @param hash last transaction hash
* @param count number of transactions to load
* @returns parsed transactions
*/
async getAccountTransactionsParsed(address: Address, lt: bigint, hash: Buffer, count: number = 20) {
let res = await axios.get(
this.#endpoint + '/account/' + address.toString({ urlSafe: true }) + '/tx/parsed/' + lt.toString(10) + '/' + toUrlSafe(hash.toString('base64')),
{
adapter: this.#adapter,
timeout: this.#timeout,
params: {
count
}
}
);
let parsedTransactionsRes = parsedTransactionsCodec.safeParse(res.data);

if (!parsedTransactionsRes.success) {
throw Error('Mailformed response');
}

return parsedTransactionsRes.data as ParsedTransactions;
}

/**
* Get network config
* @param seqno block sequence number
Expand Down Expand Up @@ -560,13 +588,140 @@ const sendCodec = z.object({
status: z.number()
});

const blocksCodec = z.array(z.object({
workchain: z.number(),
seqno: z.number(),
shard: z.string(),
rootHash: z.string(),
fileHash: z.string()
}));

const transactionsCodec = z.object({
blocks: z.array(z.object({
workchain: z.number(),
seqno: z.number(),
shard: z.string(),
rootHash: z.string(),
fileHash: z.string()
})),
blocks: blocksCodec,
boc: z.string()
});

const parsedAddressExternalCodec = z.object({
bits: z.number(),
data: z.string()
});

const parsedMessageInfoCodec = z.union([
z.object({
type: z.literal('internal'),
value: z.string(),
dest: z.string(),
src: z.string(),
bounced: z.boolean(),
bounce: z.boolean(),
ihrDisabled: z.boolean(),
createdAt: z.number(),
createdLt: z.string(),
fwdFee: z.string(),
ihrFee: z.string()
}),
z.object({
type: z.literal('external-in'),
dest: z.string(),
src: z.union([parsedAddressExternalCodec, z.null()]),
importFee: z.string()
}),
z.object({
type: z.literal('external-out'),
dest: z.union([parsedAddressExternalCodec, z.null()])
})
]);

const parsedStateInitCodec = z.object({
splitDepth: z.union([z.number(), z.null()]),
code: z.union([z.string(), z.null()]),
data: z.union([z.string(), z.null()]),
special: z.union([z.object({ tick: z.boolean(), tock: z.boolean() }), z.null()])
});

const parsedMessageCodec = z.object({
body: z.string(),
info: parsedMessageInfoCodec,
init: z.union([parsedStateInitCodec, z.null()])
});

const accountStatusCodec = z.union([z.literal('uninitialized'), z.literal('frozen'), z.literal('active'), z.literal('non-existing')]);

const txBodyCodec = z.union([
z.object({ type: z.literal('comment'), comment: z.string() }),
z.object({ type: z.literal('payload'), cell: z.string() }),
]);

const parsedOperationItemCodec = z.union([
z.object({ kind: z.literal('ton'), amount: z.string() }),
z.object({ kind: z.literal('token'), amount: z.string() })
]);

const supportedMessageTypeCodec = z.union([
z.literal('jetton::excesses'),
z.literal('jetton::transfer'),
z.literal('jetton::transfer_notification'),
z.literal('deposit'),
z.literal('deposit::ok'),
z.literal('withdraw'),
z.literal('withdraw::all'),
z.literal('withdraw::delayed'),
z.literal('withdraw::ok'),
z.literal('airdrop')
]);

const opCodec = z.object({
type: supportedMessageTypeCodec,
options: z.optional(z.record(z.string()))
});

const parsedOperationCodec = z.object({
address: z.string(),
comment: z.optional(z.string()),
items: z.array(parsedOperationItemCodec),
op: z.optional(opCodec)
});

const parsedTransactionCodec = z.object({
address: z.string(),
lt: z.string(),
hash: z.string(),
prevTransaction: z.object({
lt: z.string(),
hash: z.string()
}),
time: z.number(),
outMessagesCount: z.number(),
oldStatus: accountStatusCodec,
newStatus: accountStatusCodec,
fees: z.string(),
update: z.object({
oldHash: z.string(),
newHash: z.string()
}),
inMessage: z.union([parsedMessageCodec, z.null()]),
outMessages: z.array(parsedMessageCodec),
parsed: z.object({
seqno: z.union([z.number(), z.null()]),
body: z.union([txBodyCodec, z.null()]),
status: z.union([z.literal('success'), z.literal('failed'), z.literal('pending')]),
dest: z.union([z.string(), z.null()]),
kind: z.union([z.literal('out'), z.literal('in')]),
amount: z.string(),
resolvedAddress: z.string(),
bounced: z.boolean(),
mentioned: z.array(z.string())
}),
operation: parsedOperationCodec
});

const parsedTransactionsCodec = z.object({
blocks: blocksCodec,
transactions: z.array(parsedTransactionCodec)
});

export type ParsedTransaction = z.infer<typeof parsedTransactionCodec>;
export type ParsedTransactions = {
blocks: z.infer<typeof blocksCodec>,
transactions: ParsedTransaction[]
};
5 changes: 3 additions & 2 deletions src/client/api/HttpApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ const message = z.object({
ihr_fee: z.string(),
created_lt: z.string(),
body_hash: z.string(),
msg_data: messageData
msg_data: messageData,
message: z.string()
});

const transaction = z.object({
Expand Down Expand Up @@ -362,4 +363,4 @@ function serializeStack(src: TupleItem[]) {
}
}
return stack;
}
}
8 changes: 7 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,10 @@ export { GasLimitsPrices, StoragePrices, MsgPrices, WorkchainDescriptor,
configParseValidatorSet, configParseWorkchainDescriptor,
parseBridge, parseProposalSetup, parseValidatorSet, parseVotingSetup,
parseFullConfig,
loadConfigParamById, loadConfigParamsAsSlice } from './config/ConfigParser'
loadConfigParamById, loadConfigParamsAsSlice } from './config/ConfigParser'

//
// Fees
//

export { computeExternalMessageFees, computeFwdFees, computeGasPrices, computeMessageForwardFees, computeStorageFees } from './utils/fees';
Loading

0 comments on commit ea77703

Please sign in to comment.