diff --git a/README.md b/README.md index cfdcce7..b0d78a0 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,23 @@ ## TON (The Open Network) blockchain - The duck repo -![state of the workflow](https://github.com/fabcotech/nest-typeorm-boilerplate/actions/workflows/main.yml/badge.svg) +![state of the workflow](https://github.com/fabcotech/ton-func-contracts/actions/workflows/main.yml/badge.svg) ![The duck](https://sl.combot.org/utyaduck/webp/6xf09f98b3.webp) -A set of low, high value TON blockchain (The Open Network) FunC smart contracts and test suites. New code coming in regularly. - -- **Arithmetic** : TON Smart contract that performs basic add/substract/multiply operations on a integer -- **Fibonacci** : TON Smart contract that stores two integers, and continues the fibonacci sequence everytime it is touched. -- **Ten thousands transfers** : Two blockchain users exchange TON back and forth, 10.000 times, balance is checked after each transfer. -- **One thousand token transfer** : Mints a new token with 100B supply, 30B are sent to three blockchain users, they do one thousand small token transfers. Balance is checked after each transfer. +A set of low, high value TON blockchain (The Open Network) FunC smart contracts +and test suites. New code coming in regularly. + +- **Arithmetic** : TON Smart contract that performs basic add/substract/multiply + operations on a integer +- **Fibonacci** : TON Smart contract that stores two integers, and continues the + fibonacci sequence everytime it is touched. +- **Ten thousands transfers** : Two blockchain users exchange TON back and + forth, 10.000 times, balance is checked after each transfer. +- **One thousand token transfer** : Mints a new token with 100B supply, 30B are + sent to three blockchain users, they do one thousand small token transfers. + Balance is checked after each transfer. +- **Ping pong** : A contract stores a string (as `func slice`), each time it is + called, `"ping"` is toggled to `"pong"` and vice versa. ```sh yarn @@ -27,4 +35,7 @@ yarn test tests/TenThousandsTransfers.spec.ts # Only test 1.000 token transfers yarn test tests/OneThousandTokenTransfers.spec.ts + +# Only test ping pong +yarn test tests/PingPong.spec.ts ``` diff --git a/contracts/pingpong.fc b/contracts/pingpong.fc index dc55f5f..91c2896 100644 --- a/contracts/pingpong.fc +++ b/contracts/pingpong.fc @@ -1,64 +1,48 @@ #include "imports/stdlib.fc"; -const op::toggle = "op::toggle"c; +int slice_hash(slice s) asm "HASHSU"; +int equal_slices(slice a, slice b) asm "SDEQ"; ;; storage variables +global cell ctx_str; -;; id is required to be able to create different instances of counters -;; since addresses in TON depend on the initial state of the contract -global slice str = "ping"; - -;; load_data populates storage variables using stored data () load_data() impure { - var ds = get_data().begin_parse(); - str = ds~load_slice(); - ds.end_parse(); + var ds = get_data().begin_parse(); + ctx_str = ds~load_ref(); + ds.end_parse(); } -;; save_data stores storage variables as a cell into persistent storage () save_data() impure { - set_data( - begin_cell() - .store_slice(str) - .end_cell() - ); + set_data( + begin_cell() + .store_ref(ctx_str) + .end_cell() + ); } -;; recv_internal is the main function of the contract and is called when it receives a message from other contracts () recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure { - if (in_msg_body.slice_empty?()) { ;; ignore all empty messages - return (); - } - - slice cs = in_msg_full.begin_parse(); - int flags = cs~load_uint(4); - if (flags & 1) { ;; ignore all bounced messages - return (); - } - - load_data(); ;; here we populate the storage variables - - int op = in_msg_body~load_uint(32); ;; by convention, the first 32 bits of incoming message is the op - int query_id = in_msg_body~load_uint(64); ;; also by convention, the next 64 bits contain the "query id", although this is not always the case - - if (op == op::increase) { - slice str = = begin_cell() - .store_slice("pong") - .store_ref(null()) - .end_cell() - .begin_parse(); - save_data(); - return (); - } - - throw(0xffff); ;; if the message contains an op that is not known to this contract, we throw + if (in_msg_body.slice_empty?()) { + return (); + } + + load_data(); + slice current = ctx_str.begin_parse(); + slice ping = begin_cell().store_slice("ping").end_cell().begin_parse(); + + if (equal_slices(current,ping) == -1) { + cell c = begin_cell().store_slice("pong").end_cell(); + ctx_str = c; + save_data(); + return (); + } else { + cell c = begin_cell().store_slice("ping").end_cell(); + ctx_str = c; + save_data(); + return (); + } } -;; get methods are a means to conveniently read contract data using, for example, HTTP APIs -;; they are marked with method_id -;; note that unlike in many other smart contract VMs, get methods cannot be called by other contracts - -int get_str() method_id { - load_data(); - return str; -} +cell get_str() method_id { + load_data(); + return ctx_str; +} \ No newline at end of file diff --git a/tests/PingPong.spec.ts b/tests/PingPong.spec.ts new file mode 100644 index 0000000..846a6d4 --- /dev/null +++ b/tests/PingPong.spec.ts @@ -0,0 +1,75 @@ +import { Blockchain, SandboxContract, TreasuryContract } from '@ton/sandbox'; +import { Cell, toNano, ContractProvider } from '@ton/core'; +import '@ton/test-utils'; +import { compile } from '@ton/blueprint'; + +import { PingPong } from '../wrappers/PingPong'; + +export const Opcodes = { + toggle: 0x7e8764ef, +}; + +describe('[PingPong]', () => { + let code: Cell; + let blockchain: Blockchain; + let deployer: SandboxContract; + let pingPongContract: SandboxContract; + let user1: null | SandboxContract = null; + let provider: null | ContractProvider = null; + + beforeAll(async () => { + code = await compile('PingPong'); + blockchain = await Blockchain.create(); + user1 = await blockchain.treasury('user1'); + pingPongContract = blockchain.openContract( + PingPong.createFromConfig({}, code) + ); + provider = blockchain.provider(pingPongContract.address); + deployer = await blockchain.treasury('deployer'); + (provider as any).blockchain.openContract( + PingPong.createFromConfig({}, code) + ); + }); + + it('[PingPong] deploys', async () => { + const deployResult = await pingPongContract.sendDeploy( + deployer.getSender(), + toNano('0.05') + ); + expect(deployResult.transactions).toHaveTransaction({ + from: deployer.address, + to: pingPongContract.address, + deploy: true, + success: true, + }); + }); + + it('[PingPong] toggle', async () => { + const str = await pingPongContract.getStr(); + const strOk = (str as any).toString().replace('x{', '').replace('}', ''); + const decoded = Buffer.from(strOk, 'hex').toString('utf8'); + expect(decoded).toBe('ping'); + + await pingPongContract.sendToggle( + (user1 as SandboxContract).getSender(), + { + value: toNano('0.05'), + } + ); + const str2 = await pingPongContract.getStr(); + const str2Ok = (str2 as any).toString().replace('x{', '').replace('}', ''); + const decoded2 = Buffer.from(str2Ok, 'hex').toString('utf8'); + expect(decoded2).toBe('pong'); + + await pingPongContract.sendToggle( + (user1 as SandboxContract).getSender(), + { + value: toNano('0.05'), + } + ); + const str3 = await pingPongContract.getStr(); + const str3Ok = (str3 as any).toString().replace('x{', '').replace('}', ''); + const decoded3 = Buffer.from(str3Ok, 'hex').toString('utf8'); + expect(decoded3).toBe('ping'); + }); +}); diff --git a/wrappers/PingPong.compile.ts b/wrappers/PingPong.compile.ts new file mode 100644 index 0000000..2de4d99 --- /dev/null +++ b/wrappers/PingPong.compile.ts @@ -0,0 +1,6 @@ +import { CompilerConfig } from '@ton/blueprint'; + +export const compile: CompilerConfig = { + lang: 'func', + targets: ['contracts/pingpong.fc'], +}; diff --git a/wrappers/PingPong.ts b/wrappers/PingPong.ts new file mode 100644 index 0000000..1729ab8 --- /dev/null +++ b/wrappers/PingPong.ts @@ -0,0 +1,60 @@ +import { + Address, + beginCell, + Cell, + Contract, + contractAddress, + ContractProvider, + Sender, + SendMode, +} from '@ton/core'; + +export function pingPongConfigToCell(): Cell { + const forwardPayload = beginCell().storeStringTail('ping').endCell(); + return beginCell().storeRef(forwardPayload).endCell(); +} + +export class PingPong implements Contract { + constructor( + readonly address: Address, + readonly init?: { code: Cell; data: Cell } + ) {} + + static createFromAddress(address: Address) { + return new PingPong(address); + } + + static createFromConfig(config: object, code: Cell, workchain = 0) { + const data = pingPongConfigToCell(); + const init = { code, data }; + return new PingPong(contractAddress(workchain, init), init); + } + + async sendDeploy(provider: ContractProvider, via: Sender, value: bigint) { + const a = await provider.internal(via, { + value, + sendMode: SendMode.PAY_GAS_SEPARATELY, + body: beginCell().endCell(), + }); + return a; + } + + async sendToggle( + provider: ContractProvider, + via: Sender, + opts: { + value: bigint; + } + ) { + await provider.internal(via, { + value: opts.value, + sendMode: SendMode.PAY_GAS_SEPARATELY, + body: beginCell().storeUint(10, 32).endCell(), + }); + } + + async getStr(provider: ContractProvider) { + const result = await provider.get('get_str', []); + return result.stack.readCell(); + } +}