From f9f6bd16cfd0d443f590fc57188242e031667da7 Mon Sep 17 00:00:00 2001 From: Patricio Tourne Passarino Date: Thu, 8 Jan 2026 19:39:41 -0300 Subject: [PATCH 1/7] feat: freezing counter --- contracts/tests/Freeze.spec.ts | 206 +++++++++++++++++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100644 contracts/tests/Freeze.spec.ts diff --git a/contracts/tests/Freeze.spec.ts b/contracts/tests/Freeze.spec.ts new file mode 100644 index 000000000..c18f1a98a --- /dev/null +++ b/contracts/tests/Freeze.spec.ts @@ -0,0 +1,206 @@ +import '@ton/test-utils' +import { + Blockchain, + defaultConfig, + loadConfig, + SandboxContract, + TreasuryContract, + updateConfig, +} from '@ton/sandbox' +import { beginCell, Cell, StateInit, StorageUsed, toNano } from '@ton/core' + +import * as counter from '../wrappers/examples/Counter' +import { + GasLimitsPrices, + GasLimitsPrices_gas_flat_pfx, + GasLimitsPrices_gas_prices, + GasLimitsPrices_gas_prices_ext, +} from '@ton/sandbox/dist/config/config.tlb-gen' +import { generateRandomContractId } from '../src/utils' + +describe('Contract freezing and unfreezing', () => { + let blockchain: Blockchain + let deployer: SandboxContract + let bind: { + counter: SandboxContract + } + let code: Cell + + const dueLimits = { + freeze_due_limit: 1_000n, + delete_due_limit: 100_000n, + } + + beforeEach(async () => { + blockchain = await Blockchain.create() + blockchain.now = 1 + updateDueLimits(blockchain, dueLimits) + + const counterData = { + id: generateRandomContractId(), + value: 0, + ownable: { owner: deployer.address, pendingOwner: null }, + } + code = await counter.ContractClient.code() + deployer = await blockchain.treasury('deployer') + + bind = { + counter: blockchain.openContract( + counter.ContractClient.newFrom( + { + id: generateRandomContractId(), + value: 0, + ownable: { owner: deployer.address, pendingOwner: null }, + }, + code, + ), + ), + } + }) + + it('should freeze and unfreeze', async () => { + const initialTon = 1000000n + var lastBalance = initialTon + // Deploy + var size: StorageUsed + { + const result = await bind.counter.sendDeploy(deployer.getSender(), initialTon) + expect(result.transactions).toHaveTransaction({ + from: deployer.address, + to: bind.counter.address, + deploy: true, + success: true, + }) + console.log('Deployed') + const state = await getContractState() + logAccountState(state) + size = state.account.account!.storageStats.used + lastBalance = state.balance + expect(lastBalance).toBeLessThan(initialTon) + } + // Freeze + { + console.log('Freezing') + const probeInterval = 100_000 + warpTime(probeInterval) // Advance time by 100k seconds to accumulate rent fees + await triggerAccountStateUpdate() + + const state = await getContractState() + logAccountState(state) + expect(state.balance).toBeGreaterThan(0n) + const difference = lastBalance - state.balance + console.log(`Difference: ${difference} nanoton over ${probeInterval} sec`) + expect(difference).toBeGreaterThan(0n) + + if (!state.accountState) { + throw new Error('Account state is undefined! It probably got deleted.') + } + const rent = Number(difference) / probeInterval + console.log(`Rate: ${rent} nanoton/sec`) + + const expectedDue = (dueLimits.freeze_due_limit + dueLimits.delete_due_limit) / 2n // Must fall between freeze and delete limits + const timeToDrain = Math.floor(Number(state.balance + 1n) / rent) + console.log(`Estimated time to drain: ${timeToDrain} sec`) + warpTime(timeToDrain) + await triggerAccountStateUpdate() + + const stateDrained = await getContractState() + logAccountState(stateDrained) + + expect(stateDrained.balance).toBe(0n) + expect(stateDrained.account.account!.storageStats.duePayment).toBeNull() + + expect(stateDrained.accountState!.type).toBe('active') + + const timeToFreeze = Math.floor(Number(expectedDue + 1n) / rent) + console.log(`Estimated time to freeze: ${timeToFreeze} sec`) + warpTime(timeToFreeze) + await triggerAccountStateUpdate() + + const stateFrozen = await getContractState() + logAccountState(stateFrozen) + + expect(stateFrozen.balance).toBe(0n) + expect(stateFrozen.account.account!.storageStats.duePayment).toBeGreaterThan( + dueLimits.freeze_due_limit, + ) + expect(stateFrozen.account.account!.storageStats.duePayment).toBeLessThan( + dueLimits.delete_due_limit, + ) + + expect(['frozen', 'uninit']).toContain(stateFrozen.accountState!.type) + console.log('Contract is frozen now.') + } + + function logAccountState(state) { + console.log( + `Balance: ${state.balance} | Due: ${state.account.account!.storageStats.duePayment} | State: ${state.accountState?.type}`, + ) + } + }) + + function warpTime(period: number) { + blockchain.now = blockchain.now!! + period + } + + async function getContractState() { + return await blockchain.getContract(bind.counter.address) + } + + async function triggerAccountStateUpdate() { + const result = await bind.counter.sendInternal( + deployer.getSender(), + toNano('1'), + beginCell().storeUint(0xffffffff, 32).asCell(), + ) + expect(result.transactions).toHaveTransaction({ + from: deployer.address, + to: bind.counter.address, + success: false, // The call must fail so it bounces the TON back + }) + expect(result.transactions).toHaveTransaction({ + from: bind.counter.address, + to: deployer.address, + inMessageBounced: true, + }) + } +}) +function updateDueLimits( + blockchain: Blockchain, + dueLimits: { freeze_due_limit: bigint; delete_due_limit: bigint }, +) { + const oldConfig = loadConfig(blockchain.config) + + function fas(prevParam: GasLimitsPrices): GasLimitsPrices { + console.log('Old limits:', prevParam) + switch (prevParam.kind) { + case 'GasLimitsPrices_gas_flat_pfx': + const val: GasLimitsPrices_gas_flat_pfx = { + ...prevParam, + other: fas(prevParam.other), + } + return val + + case 'GasLimitsPrices_gas_prices': { + const val: GasLimitsPrices_gas_prices = { + ...prevParam, + ...dueLimits, + } + return val + } + case 'GasLimitsPrices_gas_prices_ext': { + const val: GasLimitsPrices_gas_prices_ext = { + ...prevParam, + ...dueLimits, + } + return val + } + } + } + const newGasLimit = fas(oldConfig[21].anon0) + const updatedConfig = updateConfig(blockchain.config, { + kind: 'ConfigParam_config_gas_prices', + anon0: newGasLimit, + }) + blockchain.setConfig(updatedConfig) +} From 608a9e6756d1ed54d8b8bb29f8642b65f690a1e7 Mon Sep 17 00:00:00 2001 From: Patricio Tourne Passarino Date: Fri, 9 Jan 2026 10:22:41 -0300 Subject: [PATCH 2/7] feat: demo with freezer contract --- .../test/examples/funding/freezer.tolk | 51 ++++ contracts/tests/Freeze.spec.ts | 235 +++++++++++++----- .../examples.funding.Freezer.compile.ts | 9 + .../wrappers/examples/funding/Freezer.ts | 180 ++++++++++++++ 4 files changed, 406 insertions(+), 69 deletions(-) create mode 100644 contracts/contracts/test/examples/funding/freezer.tolk create mode 100644 contracts/wrappers/examples.funding.Freezer.compile.ts create mode 100644 contracts/wrappers/examples/funding/Freezer.ts diff --git a/contracts/contracts/test/examples/funding/freezer.tolk b/contracts/contracts/test/examples/funding/freezer.tolk new file mode 100644 index 000000000..20ccecb23 --- /dev/null +++ b/contracts/contracts/test/examples/funding/freezer.tolk @@ -0,0 +1,51 @@ +// Small contract to test freezing and unfreezing contracts + +struct Storage { + id: uint32; + value: uint32; +} + +struct (0xed2287f4) SetValue { + queryID: uint64; + value: uint32; +} + +struct (0xfd475c25) Drain { +} + +type InMessages = + | SetValue + | Drain; + +fun onInternalMessage(in: InMessage) { + val msg = lazy InMessages.fromSlice(in.body); + match (msg) { + SetValue => { + var st = lazy Storage.fromCell(contract.getData()); + st.value = msg.value; + contract.setData(st.toCell()); + reply(in.senderAddress).send(SEND_MODE_CARRY_ALL_REMAINING_MESSAGE_VALUE); + } + Drain => { + // send all balance to sender + reply(in.senderAddress).send(SEND_MODE_CARRY_ALL_BALANCE); + } + else => { + assert (in.body.isEmpty()) throw 0xffff; + }, + } +} + +get fun value() { + val st = lazy Storage.fromCell(contract.getData()); + return st.value; +} + +fun reply(dest: address): OutMessage { + return createMessage({ + bounce: BounceMode.NoBounce, + value: 0, + dest, + body: createEmptyCell(), + }); +} \ No newline at end of file diff --git a/contracts/tests/Freeze.spec.ts b/contracts/tests/Freeze.spec.ts index c18f1a98a..422ff690e 100644 --- a/contracts/tests/Freeze.spec.ts +++ b/contracts/tests/Freeze.spec.ts @@ -1,15 +1,15 @@ import '@ton/test-utils' import { Blockchain, - defaultConfig, loadConfig, SandboxContract, + SmartContract, TreasuryContract, updateConfig, } from '@ton/sandbox' import { beginCell, Cell, StateInit, StorageUsed, toNano } from '@ton/core' -import * as counter from '../wrappers/examples/Counter' +import * as freezer from '../wrappers/examples/funding/Freezer' import { GasLimitsPrices, GasLimitsPrices_gas_flat_pfx, @@ -18,11 +18,25 @@ import { } from '@ton/sandbox/dist/config/config.tlb-gen' import { generateRandomContractId } from '../src/utils' +class CompleteStateInit { + constructor( + public code: Cell, + public data: Cell, + ) {} + + static fromStateInit(stateInit: StateInit): CompleteStateInit { + if (!(stateInit.code && stateInit.data)) { + throw new Error('StateInit is missing code or data') + } + return new CompleteStateInit(stateInit.code, stateInit.data) + } +} + describe('Contract freezing and unfreezing', () => { let blockchain: Blockchain let deployer: SandboxContract let bind: { - counter: SandboxContract + freezer: SandboxContract } let code: Cell @@ -36,21 +50,15 @@ describe('Contract freezing and unfreezing', () => { blockchain.now = 1 updateDueLimits(blockchain, dueLimits) - const counterData = { - id: generateRandomContractId(), - value: 0, - ownable: { owner: deployer.address, pendingOwner: null }, - } - code = await counter.ContractClient.code() deployer = await blockchain.treasury('deployer') + code = await freezer.ContractClient.code() bind = { - counter: blockchain.openContract( - counter.ContractClient.newFrom( + freezer: blockchain.openContract( + freezer.ContractClient.createFromConfig( { id: generateRandomContractId(), value: 0, - ownable: { owner: deployer.address, pendingOwner: null }, }, code, ), @@ -59,83 +67,165 @@ describe('Contract freezing and unfreezing', () => { }) it('should freeze and unfreeze', async () => { - const initialTon = 1000000n - var lastBalance = initialTon // Deploy - var size: StorageUsed - { - const result = await bind.counter.sendDeploy(deployer.getSender(), initialTon) + const stateInit = await (async (): Promise => { + const result = await bind.freezer.sendDeploy(deployer.getSender(), toNano('1')) expect(result.transactions).toHaveTransaction({ from: deployer.address, - to: bind.counter.address, + to: bind.freezer.address, deploy: true, success: true, }) console.log('Deployed') const state = await getContractState() logAccountState(state) - size = state.account.account!.storageStats.used - lastBalance = state.balance - expect(lastBalance).toBeLessThan(initialTon) - } + if (state.account.account!.storage.state.type !== 'active') { + throw new Error('Contract is not active after deploy!') + } + return CompleteStateInit.fromStateInit(state.account.account!.storage.state.state) + })() + + // Change Status + const { lastBalance, currentState } = await (async (): Promise<{ + lastBalance: bigint + currentState: CompleteStateInit + }> => { + const result = await bind.freezer.sendSetValue(deployer.getSender(), { + value: toNano('0.05'), + body: { + queryID: 0n, + value: 42, + }, + }) + expect(result.transactions).toHaveTransaction({ + from: deployer.address, + to: bind.freezer.address, + success: true, + }) + console.log('Changed state') + const state = await getContractState() + logAccountState(state) + if (state.account.account!.storage.state.type !== 'active') { + throw new Error('Contract is not active after state change!') + } + return { + lastBalance: state.balance, + currentState: CompleteStateInit.fromStateInit(state.account.account!.storage.state.state), + } + })() + // Freeze { console.log('Freezing') - const probeInterval = 100_000 - warpTime(probeInterval) // Advance time by 100k seconds to accumulate rent fees - await triggerAccountStateUpdate() - const state = await getContractState() - logAccountState(state) - expect(state.balance).toBeGreaterThan(0n) - const difference = lastBalance - state.balance - console.log(`Difference: ${difference} nanoton over ${probeInterval} sec`) - expect(difference).toBeGreaterThan(0n) + // Drain remaining balance to 0 + { + const result = await bind.freezer.sendDrain(deployer.getSender(), toNano('0.05')) + expect(result.transactions).toHaveTransaction({ + from: deployer.address, + to: bind.freezer.address, + success: true, + }) + expect(result.transactions).toHaveTransaction({ + from: bind.freezer.address, + to: deployer.address, + success: true, + value(x) { + console.log(`Drained value: ${x} nanotons`) + return true + }, + }) - if (!state.accountState) { - throw new Error('Account state is undefined! It probably got deleted.') + const state = await getContractState() + logAccountState(state) + expect(state.balance).toBe(0n) + expect(state.account.account!.storageStats.duePayment).toBeNull() } - const rent = Number(difference) / probeInterval - console.log(`Rate: ${rent} nanoton/sec`) - const expectedDue = (dueLimits.freeze_due_limit + dueLimits.delete_due_limit) / 2n // Must fall between freeze and delete limits - const timeToDrain = Math.floor(Number(state.balance + 1n) / rent) - console.log(`Estimated time to drain: ${timeToDrain} sec`) - warpTime(timeToDrain) - await triggerAccountStateUpdate() - - const stateDrained = await getContractState() - logAccountState(stateDrained) + // Accumulate due payments until frozen + { + const probeInterval = 1000 + warpTime(probeInterval) // Advance time by 1000 seconds to accumulate rent fees + await triggerAccountStateUpdate() + const state = await getContractState() + if (!state.accountState) { + throw new Error('Account state is undefined! It probably got deleted.') + } + expect(state.account.account!.storageStats.duePayment).toBeDefined() + const duePayments = state.account.account!.storageStats.duePayment! + expect(duePayments).toBeGreaterThan(0n) + expect(duePayments).toBeLessThan(dueLimits.delete_due_limit) - expect(stateDrained.balance).toBe(0n) - expect(stateDrained.account.account!.storageStats.duePayment).toBeNull() + const rent = Number(duePayments) / probeInterval + console.log(`Rate: ${rent} nanoton/sec`) - expect(stateDrained.accountState!.type).toBe('active') + const expectedDue = (dueLimits.freeze_due_limit + dueLimits.delete_due_limit) / 2n // Must fall between freeze and delete limits - const timeToFreeze = Math.floor(Number(expectedDue + 1n) / rent) - console.log(`Estimated time to freeze: ${timeToFreeze} sec`) - warpTime(timeToFreeze) - await triggerAccountStateUpdate() + const timeToFreeze = Math.floor(Number(expectedDue - duePayments + 1n) / rent) + console.log(`Estimated time to freeze: ${timeToFreeze} sec`) + warpTime(timeToFreeze) + await triggerAccountStateUpdate() - const stateFrozen = await getContractState() - logAccountState(stateFrozen) + const stateFrozen = await getContractState() + logAccountState(stateFrozen) - expect(stateFrozen.balance).toBe(0n) - expect(stateFrozen.account.account!.storageStats.duePayment).toBeGreaterThan( - dueLimits.freeze_due_limit, - ) - expect(stateFrozen.account.account!.storageStats.duePayment).toBeLessThan( - dueLimits.delete_due_limit, - ) + expect(stateFrozen.balance).toBe(0n) + expect(stateFrozen.account.account!.storageStats.duePayment).toBeGreaterThan( + dueLimits.freeze_due_limit, + ) + expect(stateFrozen.account.account!.storageStats.duePayment).toBeLessThan( + dueLimits.delete_due_limit, + ) - expect(['frozen', 'uninit']).toContain(stateFrozen.accountState!.type) - console.log('Contract is frozen now.') + expect(['frozen', 'uninit']).toContain(stateFrozen.accountState!.type) + console.log('Contract is frozen now.') + } } - function logAccountState(state) { - console.log( - `Balance: ${state.balance} | Due: ${state.account.account!.storageStats.duePayment} | State: ${state.accountState?.type}`, - ) + // Unfreeze + { + console.log('Test unfreezing with initial status') + { + const freezerContract = blockchain.openContract( + freezer.ContractClient.createFromFrozen(bind.freezer, stateInit), // Doesn't match current state + ) + const result = await freezerContract.sendDeploy(deployer.getSender(), toNano('0.1')) + expect(result.transactions).toHaveTransaction({ + from: deployer.address, + to: bind.freezer.address, + success: false, + exitCode: undefined, + }) + } + { + const freezerContract = blockchain.openContract( + freezer.ContractClient.createFromFrozen(bind.freezer, currentState), + ) + const result = await freezerContract.sendDeploy(deployer.getSender(), toNano('0.1')) + expect(result.transactions).toHaveTransaction({ + from: deployer.address, + to: bind.freezer.address, + success: true, + }) + console.log('Unfrozen by deploy') + const state = await getContractState() + logAccountState(state) + expect(state.account.account!.storage.state.type).toBe('active') + if (!state.account.account!.storageStats.duePayment) { + throw new Error(`Due payment should not have been cleared yet!`) + } + const netBalance = state.balance - state.account.account!.storageStats.duePayment! + + console.log('Trigger account update to pay dues') + await triggerAccountStateUpdate() // Ensure contract is operational + const state2 = await getContractState() + logAccountState(state2) + expect(state2.balance).toBe(netBalance) + + const value = await freezerContract.getValue() + expect(value).toBe(42) + console.log('Contract is unfrozen and operational again.') + } } }) @@ -144,27 +234,34 @@ describe('Contract freezing and unfreezing', () => { } async function getContractState() { - return await blockchain.getContract(bind.counter.address) + return await blockchain.getContract(bind.freezer.address) } async function triggerAccountStateUpdate() { - const result = await bind.counter.sendInternal( + const result = await bind.freezer.sendInternal( deployer.getSender(), toNano('1'), beginCell().storeUint(0xffffffff, 32).asCell(), ) expect(result.transactions).toHaveTransaction({ from: deployer.address, - to: bind.counter.address, + to: bind.freezer.address, success: false, // The call must fail so it bounces the TON back }) expect(result.transactions).toHaveTransaction({ - from: bind.counter.address, + from: bind.freezer.address, to: deployer.address, inMessageBounced: true, }) } }) + +function logAccountState(state: SmartContract) { + console.log( + `Balance: ${state.balance} | Due: ${state.account.account!.storageStats.duePayment} | State: ${state.accountState?.type}`, + ) +} + function updateDueLimits( blockchain: Blockchain, dueLimits: { freeze_due_limit: bigint; delete_due_limit: bigint }, diff --git a/contracts/wrappers/examples.funding.Freezer.compile.ts b/contracts/wrappers/examples.funding.Freezer.compile.ts new file mode 100644 index 000000000..e01e53a83 --- /dev/null +++ b/contracts/wrappers/examples.funding.Freezer.compile.ts @@ -0,0 +1,9 @@ +import { CompilerConfig } from '@ton/blueprint' + +export const compile: CompilerConfig = { + lang: 'tolk', + entrypoint: 'contracts/test/examples/funding/freezer.tolk', + withStackComments: true, // Fift output will contain comments, if you wish to debug its output + withSrcLineComments: true, // Fift output will contain .tolk lines as comments + experimentalOptions: '', // you can pass experimental compiler options here +} diff --git a/contracts/wrappers/examples/funding/Freezer.ts b/contracts/wrappers/examples/funding/Freezer.ts new file mode 100644 index 000000000..ef8b537e8 --- /dev/null +++ b/contracts/wrappers/examples/funding/Freezer.ts @@ -0,0 +1,180 @@ +import { SandboxContract } from '@ton/sandbox' +import { + Address, + beginCell, + Builder, + Cell, + Contract, + ContractABI, + contractAddress, + ContractProvider, + Sender, + SendMode, + Slice, +} from '@ton/core' +import { loadContractCode } from '../../codeLoader' + +import { CellCodec } from '../../utils' +import * as ownable2step from '../../libraries/access/Ownable2Step' +import * as typeAndVersion from '../../libraries/versioning/TypeAndVersion' +import { Maybe } from '@ton/core/dist/utils/maybe' + +/// @dev Message to set the contract value. +export type SetValue = { + queryID: bigint + value: number +} + +/// Message to drain the contract balance. +export type Drain = {} + +export const opcodes = { + in: { + SetValue: 0xed2287f4, + drain: 0xfd475c25, + }, + out: {}, +} + +export type ContractData = { + /// ID allows multiple independent instances, since contract address depends on initial state. + id: bigint | number // uint32 + value: number // uint32 +} + +export const builder = { + message: { + in: (() => { + // Creates a new `setValue` message. + const setValue: CellCodec = { + encode: (msg: SetValue): Builder => { + return beginCell() // break line + .storeUint(opcodes.in.SetValue, 32) + .storeUint(msg.queryID, 64) + .storeUint(msg.value, 32) + }, + load: (src: Slice): SetValue => { + src.skip(32) // skip opcode + return { + queryID: src.loadUintBig(64), + value: src.loadUint(32), + } + }, + } + // Creates a new `IncreaseCount` message. + const drain: CellCodec = { + encode: (msg: Drain): Builder => { + return beginCell().storeUint(opcodes.in.drain, 32) + }, + load: (src: Slice): Drain => { + src.skip(32) // skip opcode + return {} + }, + } + + return { setValue, drain } + })(), + }, + data: (() => { + const contractData: CellCodec = { + encode: (data: ContractData): Builder => { + return beginCell().storeUint(data.id, 32).storeUint(data.value, 32) + }, + load: (src: Slice): ContractData => { + return { + id: src.loadUintBig(32), + value: src.loadUint(32), + } + }, + } + + return { + contractData, + } + })(), +} + +export class ContractClient implements Contract, typeAndVersion.Interface { + constructor( + readonly address: Address, + readonly init?: { code: Cell; data: Cell }, + readonly abi?: Maybe, + ) { + this.abi = { + name: 'Freezer', + } + } + + static createFromAddress(address: Address): ContractClient { + return new ContractClient(address) + } + + static createFromConfig(data: ContractData, code: Cell, workchain = 0): ContractClient { + const init = { code, data: builder.data.contractData.encode(data).asCell() } + return new ContractClient(contractAddress(workchain, init), init) + } + + static createFromFrozen( + contract: SandboxContract, + stateInit: { code: Cell; data: Cell }, + ): any { + return new ContractClient(contract.address, stateInit) + } + + async sendInternal(p: ContractProvider, via: Sender, value: bigint, body: Cell) { + await p.internal(via, { + value: value, + sendMode: SendMode.PAY_GAS_SEPARATELY, + body: body, + }) + } + + async sendDeploy(p: ContractProvider, via: Sender, value: bigint): Promise { + const body = Cell.EMPTY + await this.sendInternal(p, via, value, body) + } + + async sendSetValue(p: ContractProvider, via: Sender, opts: { value: bigint; body: SetValue }) { + return this.sendInternal( + p, + via, + opts.value, + builder.message.in.setValue.encode(opts.body).asCell(), + ) + } + + async sendDrain(provider: ContractProvider, via: Sender, value: bigint): Promise { + await provider.internal(via, { + value, + sendMode: SendMode.PAY_GAS_SEPARATELY, + body: builder.message.in.drain.encode({}).asCell(), + }) + } + + static code(): Promise { + return loadContractCode('examples.funding.Freezer') + } + + async getValue(provider: ContractProvider): Promise { + const result = await provider.get('value', []) + return result.stack.readNumber() + } + + async getId(provider: ContractProvider): Promise { + const result = await provider.get('id', []) + return result.stack.readNumber() + } + + // Delegate TypeAndVersion methods + async getTypeAndVersion(provider: ContractProvider): Promise<{ type: string; version: string }> { + return typeAndVersion.getTypeAndVersion(provider) + } + + async getCode(provider: ContractProvider): Promise { + return typeAndVersion.getCode(provider) + } + + async getCodeHash(provider: ContractProvider): Promise { + return typeAndVersion.getCodeHash(provider) + } +} From a1eb1f4fa2e8a48c4a1a0ab6cc9abc1a7ab560eb Mon Sep 17 00:00:00 2001 From: Patricio Tourne Passarino Date: Fri, 9 Jan 2026 11:07:07 -0300 Subject: [PATCH 3/7] ref: move balance monitor --- integration-tests/monitor/balance_test.go | 2 +- pkg/relay/chain.go | 4 ++-- pkg/relay/monitor/{ => balance}/balance.go | 2 +- pkg/relay/monitor/{ => balance}/balance_test.go | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) rename pkg/relay/monitor/{ => balance}/balance.go (99%) rename pkg/relay/monitor/{ => balance}/balance_test.go (99%) diff --git a/integration-tests/monitor/balance_test.go b/integration-tests/monitor/balance_test.go index a405d5086..6591d9f13 100644 --- a/integration-tests/monitor/balance_test.go +++ b/integration-tests/monitor/balance_test.go @@ -17,7 +17,7 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/monitoring/balance" test_utils "github.com/smartcontractkit/chainlink-ton/deployment/utils" - "github.com/smartcontractkit/chainlink-ton/pkg/relay/monitor" + monitor "github.com/smartcontractkit/chainlink-ton/pkg/relay/monitor/balance" relayer_utils "github.com/smartcontractkit/chainlink-ton/pkg/relay/testutils" "github.com/smartcontractkit/chainlink-ton/pkg/ton/tracetracking" "github.com/smartcontractkit/chainlink-ton/pkg/ton/tvm" diff --git a/pkg/relay/chain.go b/pkg/relay/chain.go index 56eff164d..e0d09bac5 100644 --- a/pkg/relay/chain.go +++ b/pkg/relay/chain.go @@ -35,7 +35,7 @@ import ( "github.com/smartcontractkit/chainlink-ton/pkg/logpoller" txloader "github.com/smartcontractkit/chainlink-ton/pkg/logpoller/loader" lppgstore "github.com/smartcontractkit/chainlink-ton/pkg/logpoller/store/postgres" - "github.com/smartcontractkit/chainlink-ton/pkg/relay/monitor" + balancemonitor "github.com/smartcontractkit/chainlink-ton/pkg/relay/monitor/balance" tonchain "github.com/smartcontractkit/chainlink-ton/pkg/ton/chain" tonconfig "github.com/smartcontractkit/chainlink-ton/pkg/ton/config" "github.com/smartcontractkit/chainlink-ton/pkg/ton/tracetracking" @@ -161,7 +161,7 @@ func newChain(cfg *config.TOMLConfig, loopKs loop.Keystore, lggr logger.Logger, return nil, fmt.Errorf("failed to get chain info for balance monitor: %w", err) } - ch.bm, err = monitor.NewBalanceMonitor(monitor.BalanceMonitorOpts{ + ch.bm, err = balancemonitor.NewBalanceMonitor(balancemonitor.BalanceMonitorOpts{ ChainInfo: balance.ChainInfo{ ChainFamilyName: chainInfo.FamilyName, ChainID: chainInfo.ChainID, diff --git a/pkg/relay/monitor/balance.go b/pkg/relay/monitor/balance/balance.go similarity index 99% rename from pkg/relay/monitor/balance.go rename to pkg/relay/monitor/balance/balance.go index 9860c5a94..89194f0d4 100644 --- a/pkg/relay/monitor/balance.go +++ b/pkg/relay/monitor/balance/balance.go @@ -1,4 +1,4 @@ -package monitor +package balance import ( "context" diff --git a/pkg/relay/monitor/balance_test.go b/pkg/relay/monitor/balance/balance_test.go similarity index 99% rename from pkg/relay/monitor/balance_test.go rename to pkg/relay/monitor/balance/balance_test.go index 874355416..1d08cd99d 100644 --- a/pkg/relay/monitor/balance_test.go +++ b/pkg/relay/monitor/balance/balance_test.go @@ -1,4 +1,4 @@ -package monitor +package balance import ( "crypto/ed25519" From c862e991ca37b801c5d7b55437e16007ec188935 Mon Sep 17 00:00:00 2001 From: Patricio Tourne Passarino Date: Fri, 9 Jan 2026 14:45:30 -0300 Subject: [PATCH 4/7] fix: golint: type name will be used as balance.BalanceMonitorOpts by other packages, and that stutters --- pkg/relay/chain.go | 2 +- pkg/relay/monitor/balance/balance.go | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/relay/chain.go b/pkg/relay/chain.go index e0d09bac5..8afadecfa 100644 --- a/pkg/relay/chain.go +++ b/pkg/relay/chain.go @@ -161,7 +161,7 @@ func newChain(cfg *config.TOMLConfig, loopKs loop.Keystore, lggr logger.Logger, return nil, fmt.Errorf("failed to get chain info for balance monitor: %w", err) } - ch.bm, err = balancemonitor.NewBalanceMonitor(balancemonitor.BalanceMonitorOpts{ + ch.bm, err = balancemonitor.NewMonitor(balancemonitor.MonitorOpts{ ChainInfo: balance.ChainInfo{ ChainFamilyName: chainInfo.FamilyName, ChainID: chainInfo.ChainID, diff --git a/pkg/relay/monitor/balance/balance.go b/pkg/relay/monitor/balance/balance.go index 89194f0d4..a54334857 100644 --- a/pkg/relay/monitor/balance/balance.go +++ b/pkg/relay/monitor/balance/balance.go @@ -19,8 +19,8 @@ import ( tonconfig "github.com/smartcontractkit/chainlink-ton/pkg/ton/config" ) -// BalanceMonitorOpts contains the options for creating a new TON account balance monitor. -type BalanceMonitorOpts struct { +// MonitorOpts contains the options for creating a new TON account balance monitor. +type MonitorOpts struct { ChainInfo balance.ChainInfo Config balance.GenericBalanceConfig @@ -29,8 +29,8 @@ type BalanceMonitorOpts struct { NewClient func(context.Context) (*ton.APIClient, error) } -// NewBalanceMonitor returns a balance monitoring services.Service which reports balance of all Keystore accounts. -func NewBalanceMonitor(opts BalanceMonitorOpts) (services.Service, error) { +// NewMonitor returns a balance monitoring services.Service which reports balance of all Keystore accounts. +func NewMonitor(opts MonitorOpts) (services.Service, error) { return balance.NewGenericBalanceMonitor(balance.GenericBalanceMonitorOpts{ ChainInfo: opts.ChainInfo, ChainNativeCurrency: "TON", From c142394e964ea8ab3c557743ed308508b291257d Mon Sep 17 00:00:00 2001 From: Patricio Tourne Passarino Date: Fri, 9 Jan 2026 15:32:13 -0300 Subject: [PATCH 5/7] ref: unsued variable --- contracts/tests/Freeze.spec.ts | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/contracts/tests/Freeze.spec.ts b/contracts/tests/Freeze.spec.ts index 422ff690e..60179b1eb 100644 --- a/contracts/tests/Freeze.spec.ts +++ b/contracts/tests/Freeze.spec.ts @@ -86,10 +86,7 @@ describe('Contract freezing and unfreezing', () => { })() // Change Status - const { lastBalance, currentState } = await (async (): Promise<{ - lastBalance: bigint - currentState: CompleteStateInit - }> => { + const currentState = await (async (): Promise => { const result = await bind.freezer.sendSetValue(deployer.getSender(), { value: toNano('0.05'), body: { @@ -108,10 +105,7 @@ describe('Contract freezing and unfreezing', () => { if (state.account.account!.storage.state.type !== 'active') { throw new Error('Contract is not active after state change!') } - return { - lastBalance: state.balance, - currentState: CompleteStateInit.fromStateInit(state.account.account!.storage.state.state), - } + return CompleteStateInit.fromStateInit(state.account.account!.storage.state.state) })() // Freeze @@ -151,6 +145,7 @@ describe('Contract freezing and unfreezing', () => { if (!state.accountState) { throw new Error('Account state is undefined! It probably got deleted.') } + logAccountState(state) expect(state.account.account!.storageStats.duePayment).toBeDefined() const duePayments = state.account.account!.storageStats.duePayment! expect(duePayments).toBeGreaterThan(0n) From 0eee88a3b7b614171db0196493ce05207f7a596b Mon Sep 17 00:00:00 2001 From: Patricio Tourne Passarino Date: Fri, 9 Jan 2026 15:36:11 -0300 Subject: [PATCH 6/7] fix: lint --- integration-tests/monitor/balance_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/integration-tests/monitor/balance_test.go b/integration-tests/monitor/balance_test.go index 6591d9f13..8e0b66ede 100644 --- a/integration-tests/monitor/balance_test.go +++ b/integration-tests/monitor/balance_test.go @@ -126,7 +126,7 @@ func TestBalanceMonitor_Polling(t *testing.T) { keystore.AddKey(tonChain.Wallet.PrivateKey()) require.NotNil(t, keystore) - opts := monitor.BalanceMonitorOpts{ + opts := monitor.MonitorOpts{ ChainInfo: balance.ChainInfo{ ChainFamilyName: "ton", ChainID: string(chainsel.TON_LOCALNET.ChainID), @@ -144,7 +144,7 @@ func TestBalanceMonitor_Polling(t *testing.T) { } // Create and start balance monitor - balanceMonitor, err := monitor.NewBalanceMonitor(opts) + balanceMonitor, err := monitor.NewMonitor(opts) require.NoError(t, err) require.NotNil(t, balanceMonitor) err = balanceMonitor.Start(t.Context()) From af71ab161aa2cc67cd43a0da9e8b879ef8347a4a Mon Sep 17 00:00:00 2001 From: Patricio Tourne Passarino Date: Mon, 12 Jan 2026 18:41:36 -0300 Subject: [PATCH 7/7] wip --- pkg/relay/monitor/state/state.go | 73 ++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 pkg/relay/monitor/state/state.go diff --git a/pkg/relay/monitor/state/state.go b/pkg/relay/monitor/state/state.go new file mode 100644 index 000000000..51924e34f --- /dev/null +++ b/pkg/relay/monitor/state/state.go @@ -0,0 +1,73 @@ +package state + +import ( + "context" + + "github.com/xssnick/tonutils-go/address" + "github.com/xssnick/tonutils-go/ton" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/services" + "github.com/smartcontractkit/chainlink-common/pkg/types/core" + "github.com/smartcontractkit/chainlink-ton/pkg/logpoller" +) + +type ServiceOptions struct { + ContractsToMonitor []address.Address + + LogPoller logpoller.Service + Logger logger.Logger + KeyStore core.Keystore +} + +func NewService(opts ServiceOptions) (services.Service, error) { + return service{ + Service: nil, + eng: &services.Engine{}, + lggr: logger.Sugared(opts.Logger), + clientProvider: func(context.Context) (ton.APIClientWrapped, error) { + panic("TODO") + }, + chainID: "", + s: nil, + }, nil +} + +type service struct { + services.Service + eng *services.Engine // Service engine for lifecycle management + lggr logger.SugaredLogger // Logger instance + clientProvider func(context.Context) (ton.APIClientWrapped, error) // TON blockchain client lazy getter + chainID string // Target chain ID + + s logpoller.Service // Log poller service +} + +// Close implements services.Service. +func (s service) Close() error { + panic("unimplemented") +} + +// HealthReport implements services.Service. +func (s service) HealthReport() map[string]error { + panic("unimplemented") +} + +// Name implements services.Service. +func (s service) Name() string { + return s.lggr.Name() + +} + +// Ready implements services.Service. +func (s service) Ready() error { + panic("unimplemented") +} + +// Start implements services.Service. +func (s service) Start(context.Context) error { + s.Service, s.eng = services.Config{ + Name: "TONLogPoller", + Start: s.start, + }.NewServiceEngine(lggr) +}