Skip to content

Commit ce2b02a

Browse files
committed
debug proxy op, add proxy op test, update readme
Signed-off-by: master_jedy <[email protected]>
1 parent 72a062c commit ce2b02a

File tree

6 files changed

+225
-36
lines changed

6 files changed

+225
-36
lines changed

price_feeds/ton/pyth-connector/README.md

Lines changed: 56 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,74 @@
11
# Pyth-connector example
2-
Provides onchain-getter: **User -> User JettonWallet -> App -> Pyth -> App -> ...** and proxy call: **User -> Pyth -> App -> ...** pyth usage examples.
2+
This example provides two main usage patterns for interacting with Pyth:
33

4-
This example can be used as a standalone module that provides tools for sandbox testing by exporting functions for deploying and configuring a local Pyth contract.
4+
- **On-chain getter:**
5+
`User → User JW → App JW → App → Pyth → App → ...`
56

6-
It demonstrates techniques for using the Pyth oracle in financial applications.
7+
- **Proxy call:**
8+
`User → Pyth → App → ...`
79

8-
The demonstration is fully sandboxed and does not require real on-chain contracts on either testnet or mainnet.
9-
Using the Hermes client is also not required — prices can be generated locally, for example: **{TON: 3.12345, USDC: 0.998, USDT: 0.999}**.
10+
You can use this example as a standalone module for sandbox testing. It exports functions to deploy and configure a local Pyth contract, making it easy to experiment without connecting to real on-chain contracts on testnet or mainnet.
1011

11-
This is achieved by using a patched Pyth contract that accepts simplified prices without a Merkle trie proof, and therefore does not verify the authenticity of the prices.
12-
Important: This patched contract is intended for testing purposes only. The production version must use the authentic Pyth contract deployed on the mainnet.
12+
This example uses a patched Pyth contract intended only for testing purposes in a local development environment. It accepts simplified prices without requiring a Merkle trie proof, so price authenticity is not verified.
1313

14-
## Project structure
14+
<div style="border-radius: 8px; border: 1px solid #ffd700; background: #fffbe6; padding: 16px; margin: 16px 0;">
15+
<strong>Important:</strong> This patched contract is for <strong>testing purposes only</strong>. For production, always use the official Pyth contract deployed on mainnet.
16+
</div>
17+
18+
The demonstration is fully sandboxed. You do not need the Hermes client, prices can be generated locally, for example: **`{ TON: 3.456, USDC: 0.998, USDT: 0.999 }`**
19+
20+
## Project Structure
21+
22+
- `contracts/` — Source code of the smart contracts and their dependencies.
23+
- `wrappers/` — TypeScript contract wrappers (implementing `Contract` from ton-core), including serialization/deserialization and compilation utilities.
24+
- `tests/` — Unit and integration tests for the contracts.
25+
- `scripts/` — Deployment and utility scripts.
1526

16-
- `contracts` - source code of all the smart contracts of the project and their dependencies.
17-
- `wrappers` - wrapper classes (implementing `Contract` from ton-core) for the contracts, including any [de]serialization primitives and compilation functions.
18-
- `tests` - tests for the contracts.
19-
- `scripts` - scripts used by the project, mainly the deployment scripts.
2027

2128
## How to use
22-
First you need to install dependencies, node v22 is required, you can use nvm to install it: `nvm use 22` .
23-
Then install dependencies, just run `yarn`
29+
30+
1. **Install Node.js version 22**
31+
It is recommended to use [nvm](https://github.com/nvm-sh/nvm) for managing Node.js versions.
32+
If you don't have nvm installed, run:
33+
```bash
34+
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
35+
source ~/.bashrc
36+
```
37+
Then install and use Node.js 22:
38+
```bash
39+
nvm install 22
40+
nvm use 22
41+
```
42+
43+
2. **Install Yarn** (if not already installed):
44+
```bash
45+
npm install -g yarn
46+
```
47+
48+
3. **Install project dependencies**
49+
In the project root directory, run:
50+
```bash
51+
yarn install
52+
```
53+
54+
After these steps, you are ready to build, test, and use the example. See the sections below for more commands.
2455

2556
### Build
26-
to build the module you can run `yarn build`
57+
to build the module you can run:
58+
```bash
59+
yarn build
60+
```
2761

2862
### Contracts
29-
To prebuild contracts run`yarn contracts`
63+
To prebuild contracts run:
64+
```bash
65+
yarn contracts
66+
```
3067

3168
### Test
32-
`yarn test:unit`
69+
```bash
70+
yarn test:unit
71+
```
3372

3473
### Deploy
3574
You don't need to deploy this example's contracts to testnet/mainnet,

price_feeds/ton/pyth-connector/contracts/PythConnector/constants/constants.fc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ const int RESERVE::REGULAR = 0;
2222
const int RESERVE::AT_MOST = 2;
2323
;;; in the case of action fail - bounce transaction. No effect if RESERVE_AT_MOST (+2) is used. TVM UPGRADE 2023-07.
2424
const int RESERVE::BOUNCE_ON_ACTION_FAIL = 16;
25+
;;; Creates an output action which would reserve all the remaining balance of the current smart contract (instead of the value originally indicated in the message).
26+
const int RESERVE::APPEND_BALANCE = 4;
2527

2628

2729
const int FEE::SUPPLY_MARGIN = 100000000; ;; 0.1 TON margin

price_feeds/ton/pyth-connector/contracts/PythConnector/operations/proxy_operation.fc

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,15 @@
1414
return (transferred_amount);
1515
}
1616

17-
;; todo: test, debug
1817
() proxy_operation_process(int my_balance, int msg_value, slice initial_sender, cell prices_dict, int query_id, cell operation_payload) impure {
1918
var (transferred_amount) = parse_proxy_operation_payload(operation_payload);
20-
throw_unless(ERROR::NOT_ENOUGH_TRANSFERRED, msg_value < transferred_amount + FEE::SUPPLY_MARGIN);
2119

22-
raw_reserve(my_balance + transferred_amount, RESERVE::REGULAR);
20+
;; check that the msg value is enough to cover the transferred amount and supply margin,
21+
;; which is supposed to be pyth connector fee
22+
throw_unless(ERROR::NOT_ENOUGH_TRANSFERRED, msg_value >= transferred_amount + FEE::SUPPLY_MARGIN);
23+
24+
;; reserve the transferred amount
25+
raw_reserve(transferred_amount, RESERVE::APPEND_BALANCE);
2326

2427
emit_log(LOG::PROXY_OPERATION_PROCESSING,
2528
begin_cell()
@@ -32,6 +35,6 @@
3235
send_message(
3336
initial_sender, 0,
3437
begin_cell().store_query_id(query_id).store_ref(makeTextBody("Thank you!")).end_cell(),
35-
SENDMODE::CARRY_ALL_BALANCE
38+
SENDMODE::CARRY_ALL_BALANCE ;; only balance remaining after reserve is sent to the initial sender
3639
);
3740
}

price_feeds/ton/pyth-connector/contracts/PythConnector/pyth_connector.fc

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,6 @@
100100
}
101101

102102
throw(ERROR::UNSUPPORTED_OPERATION);
103-
104103
}
105104

106105
int query_id = in_msg_body~load_query_id();

price_feeds/ton/pyth-connector/tests/PythConnector.spec.ts

Lines changed: 103 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Blockchain, EventAccountCreated, SandboxContract, TreasuryContract } from '@ton/sandbox';
2-
import { Address, beginCell, Cell, Dictionary, toNano } from '@ton/core';
2+
import { Address, beginCell, Cell, Dictionary, SendMode, toNano } from '@ton/core';
33
import { PythConnector } from '../wrappers/PythConnector';
44
import '@ton/test-utils';
55

@@ -16,7 +16,7 @@ import {
1616
TEST_FEED_NAMES, TEST_FEEDS, TEST_FEEDS_MAP
1717
} from "./utils/assets";
1818
import { packNamedPrices } from "./utils/prices";
19-
import { makeOnchainGetterPayload, makeTransferMessage } from "./utils/messages";
19+
import { makeOnchainGetterPayload, makePythProxyMessage, makePythProxyPayloadMessage, makeTransferMessage, parsePythProxyLogBody } from "./utils/messages";
2020
import { EventMessageSent } from "@ton/sandbox/dist/event/Event";
2121

2222
dotenv.config();
@@ -27,10 +27,6 @@ const HERMES_ENDPOINT = 'https://hermes.pyth.network';
2727
export type OraclesInfo = {
2828
pythAddress: Address;
2929
feedsMap: Dictionary<bigint, Buffer>,
30-
// pricesTtl: number,
31-
// pythComputeBaseGas: bigint,
32-
// pythComputePerUpdateGas: bigint,
33-
// pythSingleUpdateFee: bigint,
3430
};
3531

3632
describe('Generated Prices', () => {
@@ -137,10 +133,6 @@ describe('PythConnector', () => {
137133
oraclesInfo = {
138134
feedsMap: TEST_FEEDS_MAP,
139135
pythAddress: pyth.address,
140-
// pricesTtl: 180,
141-
// pythComputeBaseGas: 100n,
142-
// pythComputePerUpdateGas: 200n,
143-
// pythSingleUpdateFee: 300n,
144136
} as OraclesInfo;
145137

146138
pythConnector = await deployPythConnector(blockchain, deployer, pyth.address);
@@ -254,7 +246,107 @@ describe('PythConnector', () => {
254246
});
255247
})
256248

257-
it.skip('should succeed pyth proxy operation', async () => {
249+
it('should succeed pyth proxy operation', async () => {
250+
await pythConnector.sendConfigure(deployer.getSender(),{
251+
value: toNano('0.05'),
252+
pythAddress: pyth.address,
253+
feedsMap: TEST_FEEDS_MAP
254+
});
255+
256+
// technically, prices are not important for current implementation of pyth proxy operation
257+
// but we pass them to pyth contract for demonstration purposes
258+
const updateDataCell = packNamedPrices({TON: 3.1, USDT: 0.99, USDC: 1.01}, () => blockchain.now!);
259+
const pythPriceIds = composeFeedsCell(defaultFeeds);
260+
261+
const queryId = 0x123456n;
262+
263+
const pythFee = toNano('0.0234');
264+
const transferredAmount = toNano('0.5');
265+
const supplyMargin = toNano('0.1');
266+
267+
// need to cover pyth fee, transferred amount and supply margin
268+
const sentValue = pythFee + transferredAmount + supplyMargin;
269+
270+
const proxyPayload = makePythProxyPayloadMessage({
271+
queryId,
272+
transferredAmount,
273+
});
274+
275+
const minPublishTime = blockchain.now! - 10;
276+
const maxPublishTime = blockchain.now! + 180;
277+
278+
const msgToPyth = makePythProxyMessage({
279+
updateDataCell,
280+
pythPriceIds,
281+
minPublishTime,
282+
maxPublishTime,
283+
targetAddress: pythConnector.address,
284+
queryId,
285+
proxyPayload,
286+
transferredAmount,
287+
});
288+
console.log({
289+
'Alice wallet': aliceWallet.address,
290+
'Pyth contract': pyth.address,
291+
'Pyth connector': pythConnector.address
292+
});
293+
294+
const balanceBefore = (await blockchain.getContract(pythConnector.address)).balance;
295+
296+
const result = await aliceWallet.send({
297+
value: sentValue,
298+
to: pyth.address,
299+
sendMode: SendMode.PAY_GAS_SEPARATELY + SendMode.IGNORE_ERRORS,
300+
body: msgToPyth,
301+
});
302+
303+
const balanceAfter = (await blockchain.getContract(pythConnector.address)).balance;
304+
305+
// alice sent message to pyth
306+
expect(result.transactions).toHaveTransaction({
307+
from: aliceWallet.address,
308+
to: pyth.address,
309+
success: true,
310+
});
311+
312+
// pyth validated feeds and sent prices to pyth connector
313+
expect(result.transactions).toHaveTransaction({
314+
from: pyth.address,
315+
to: pythConnector.address,
316+
success: true,
317+
});
318+
319+
// Check that the pyth-connector balance increased by the transferred amount (within a small tolerance)
320+
const expectedBalance = balanceBefore + transferredAmount;
321+
const delta = Math.abs(Number(balanceAfter - expectedBalance));
322+
console.log({
323+
balanceBefore,
324+
balanceAfter,
325+
expectedBalance,
326+
delta
327+
});
328+
329+
expect(delta).toBeLessThanOrEqual(Number(toNano('0.00001')));
330+
331+
// remaining balance sent back to alice
332+
expect(result.transactions).toHaveTransaction({
333+
from: pythConnector.address,
334+
to: aliceWallet.address,
335+
success: true,
336+
});
337+
338+
console.log('externals: ', result.externals);
339+
340+
// expect connector emitted proxy-operation log into externals
341+
const LOG_PROXY_OPERATION_PROCESSING = 128 + 12;
342+
const hasProxyLog = result.externals.some((m: any) => {
343+
const src = (m.info as any).src?.toString?.();
344+
if (src !== pythConnector.address.toString()) return false;
345+
const logInfo = parsePythProxyLogBody(m.body);
346+
console.log('logInfo: ', logInfo);
347+
return logInfo.op === LOG_PROXY_OPERATION_PROCESSING;
348+
});
258349

350+
expect(hasProxyLog).toBe(true);
259351
})
260352
});

price_feeds/ton/pyth-connector/tests/utils/messages.ts

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Address, beginCell, Cell } from "@ton/core";
1+
import { Address, beginCell, Cell, Dictionary } from "@ton/core";
22
import { JETTON_OPCODES, OPCODES } from "./opcodes";
33

44
export function makeOnchainGetterPayload(args: {
@@ -16,7 +16,7 @@ export function makeOnchainGetterPayload(args: {
1616
.storeUint(args.maxStaleness, 64)
1717
.storeRef(args.updateDataCell)
1818
.storeRef(args.pythPriceIds)
19-
.storeRef(beginCell().endCell())
19+
.storeRef(args.operationBody)
2020
.endCell();
2121
}
2222

@@ -40,4 +40,58 @@ export function makeTransferMessage(args: {
4040
.storeBit(1)
4141
.storeRef(args.notificationBody)
4242
.endCell();
43-
}
43+
}
44+
45+
export function makePythProxyPayloadMessage(args: {
46+
queryId: number | bigint,
47+
transferredAmount: number | bigint,
48+
}) {
49+
return beginCell()
50+
.storeUint(OPCODES.PROXY_OPERATION, 32)
51+
.storeUint(args.queryId, 64)
52+
.storeRef(
53+
beginCell()
54+
.storeUint(args.transferredAmount, 64)
55+
.endCell()
56+
)
57+
.endCell();
58+
}
59+
60+
export function parsePythProxyLogBody(cell: Cell): {
61+
queryId: number,
62+
op: number,
63+
dictCell: Cell,
64+
} {
65+
const s = cell.beginParse();
66+
const op = s.loadUint(8);
67+
expect(s.remainingRefs).toBeGreaterThan(0);
68+
const s2 = s.loadRef().beginParse();
69+
const queryId = Number(s2.loadUintBig(64));
70+
const dictCell = s2.loadRef();
71+
72+
return { queryId, op, dictCell };
73+
}
74+
75+
export const PYTH_UPDATE_FEEDS_OP_CODE = 5;
76+
77+
export function makePythProxyMessage(args: {
78+
updateDataCell: Cell,
79+
pythPriceIds: Cell,
80+
minPublishTime: number,
81+
maxPublishTime: number,
82+
targetAddress: Address,
83+
queryId: number | bigint,
84+
proxyPayload: Cell,
85+
transferredAmount: number | bigint,
86+
}) {
87+
return beginCell()
88+
.storeUint(PYTH_UPDATE_FEEDS_OP_CODE, 32)
89+
.storeRef(args.updateDataCell)
90+
.storeRef(args.pythPriceIds)
91+
.storeUint(args.minPublishTime, 64)
92+
.storeUint(args.maxPublishTime, 64)
93+
.storeAddress(args.targetAddress)
94+
.storeRef(args.proxyPayload)
95+
.endCell();
96+
}
97+

0 commit comments

Comments
 (0)