diff --git a/contract/src/jetton/JettonTransfer.kt b/contract/src/jetton/JettonTransfer.kt new file mode 100644 index 00000000..cbb595f3 --- /dev/null +++ b/contract/src/jetton/JettonTransfer.kt @@ -0,0 +1,59 @@ +package org.ton.contract.jetton + +import org.ton.bigint.toBigInt +import org.ton.block.Coins +import org.ton.block.MsgAddressInt +import org.ton.cell.Cell +import org.ton.cell.CellBuilder +import org.ton.cell.CellSlice +import org.ton.tlb.TlbConstructor + +public const val OP_JETTON_TRANSFER: Int = 0xf8a7ea5 + +public data class JettonTransfer( + val queryId: ULong, + val amount: Coins, + val toAddress: MsgAddressInt, + val responseAddress: MsgAddressInt, + val forwardAmount: Coins, + val forwardPayload: Cell?, + val customPayload: Cell? +) { + public companion object : TlbConstructor( + "jetton_transfer query_id:uint64 amount:coins to_address:MsgAddress response_address:MsgAddress custom_payload:Maybe ^Cell forward_amount:coins forward_payload:Maybe ^Cell = JettonTransfer" + ) { + override fun loadTlb(cellSlice: CellSlice): JettonTransfer { + val opCode = cellSlice.loadUInt(32) + require(opCode == OP_JETTON_TRANSFER.toBigInt()) { "Invalid op code" } + + val queryId = cellSlice.loadULong() + val amount = Coins.loadTlb(cellSlice) + val toAddress = MsgAddressInt.loadTlb(cellSlice) + val responseAddress = MsgAddressInt.loadTlb(cellSlice) + val customPayload = cellSlice.loadNullableRef() + val forwardAmount = Coins.loadTlb(cellSlice) + val forwardPayload = cellSlice.loadNullableRef() + + return JettonTransfer( + queryId = queryId, + amount = amount, + toAddress = toAddress, + responseAddress = responseAddress, + forwardAmount = forwardAmount, + forwardPayload = forwardPayload, + customPayload = customPayload + ) + } + + override fun storeTlb(cellBuilder: CellBuilder, value: JettonTransfer) { + cellBuilder.storeUInt(OP_JETTON_TRANSFER, 32) + cellBuilder.storeULong(value.queryId) + Coins.storeTlb(cellBuilder, value.amount) + MsgAddressInt.storeTlb(cellBuilder, value.toAddress) + MsgAddressInt.storeTlb(cellBuilder, value.responseAddress) + cellBuilder.storeNullableRef(value.customPayload) + Coins.storeTlb(cellBuilder, value.forwardAmount) + cellBuilder.storeNullableRef(value.forwardPayload) + } + } +} diff --git a/contract/test/jetton/JettonTransfer.kt b/contract/test/jetton/JettonTransfer.kt new file mode 100644 index 00000000..cbc05dd7 --- /dev/null +++ b/contract/test/jetton/JettonTransfer.kt @@ -0,0 +1,35 @@ +package org.ton.contract.jetton + +import kotlinx.io.bytestring.encodeToByteString +import org.ton.block.AddrStd +import org.ton.block.Coins +import org.ton.cell.CellBuilder +import org.ton.tlb.storeTlb +import kotlin.UInt +import kotlin.test.Test +import kotlin.test.assertEquals + +class JettonTransferTest { + val transferJettonData = JettonTransfer(543u, Coins.ofNano(63546), AddrStd.parse("0QCLoGOnQ7fegGD5yLDk77QrH-I005hl_JlqMnXAyRyEJ8h0"), AddrStd.parse("0QBqtS9bao0LH0DxJYIPAGuwx8aRXuMmTxigj43E-Ef2Bl0o"), + Coins.ofNano(789), CellBuilder().storeUInt32(0u).storeByteString("Hello, this is a comment".encodeToByteString()).endCell(), null) + + @Test + fun testJettonTransferDataEncodeAndDecode() { + val cell = CellBuilder().storeTlb(JettonTransfer, transferJettonData).endCell() + + val decodedJttonTransferData = JettonTransfer.loadTlb(cell) + + assertEquals(transferJettonData.queryId, decodedJttonTransferData.queryId) + assertEquals(transferJettonData.amount, decodedJttonTransferData.amount) + assertEquals(transferJettonData.toAddress, decodedJttonTransferData.toAddress) + assertEquals(transferJettonData.responseAddress, decodedJttonTransferData.responseAddress) + assertEquals(transferJettonData.forwardAmount, decodedJttonTransferData.forwardAmount) + assertEquals(transferJettonData.forwardPayload, decodedJttonTransferData.forwardPayload) + } + + @Test + fun testJettonTransferTataEncode() { + val cell = CellBuilder().storeTlb(JettonTransfer, transferJettonData) + assertEquals(cell.toString(), "x{0F8A7EA5000000000000021F2F83A8011740C74E876FBD00C1F39161C9DF68563FC469A730CBF932D464EB819239084F001AAD4BD6DAA342C7D03C496083C01AEC31F1A457B8C993C62823E3713E11FD8184062B}\n x{0000000048656C6C6F2C2074686973206973206120636F6D6D656E74}") + } +} diff --git a/contract/test/jetton/JettonTransferExample.kt b/contract/test/jetton/JettonTransferExample.kt new file mode 100644 index 00000000..d577d65f --- /dev/null +++ b/contract/test/jetton/JettonTransferExample.kt @@ -0,0 +1,83 @@ +package org.ton.contract.wallet + +import io.github.andreypfau.kotlinx.crypto.sha2.sha256 +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking +import org.ton.api.pk.PrivateKeyEd25519 +import org.ton.block.AddrStd +import org.ton.block.Coins +import org.ton.block.MsgAddressInt +import org.ton.cell.CellBuilder +import org.ton.contract.jetton.JettonTransfer +import org.ton.kotlin.account.Account +import org.ton.kotlin.message.MessageLayout +import org.ton.tlb.storeTlb +import kotlin.test.Test + +class JettonTransferExample { + @Test + fun walletV4Example(): Unit = runBlocking { + val liteClient = liteClientTestnet() + + val pk = PrivateKeyEd25519(sha256("example-key".encodeToByteArray())) + + val contract = WalletV4R2Contract( + liteClient, + WalletV4R2Contract.address(pk) + ) + val testnetNonBounceAddr = + contract.address.toString(userFriendly = true, testOnly = true, bounceable = false) + println("Wallet Address: $testnetNonBounceAddr") + + var accountState = liteClient.getAccountState(contract.address) + val account = accountState.account.value as? Account + if (account == null) { + println("Account $testnetNonBounceAddr not initialized") + return@runBlocking + } + + val balance = account.storage.balance.coins + println("Account balance: $balance toncoins") + + val toAddress = AddrStd("0QBFbLhcjyVqeLgYgNxroeXr5eaNXe4l4l3ekU4xLS57-cER"); + + val jettonData = JettonTransfer( + queryId = ULongRange(ULong.MIN_VALUE, ULong.MAX_VALUE).random(), + amount = Coins.ofNano(1), + toAddress = toAddress, + responseAddress = contract.address, + forwardAmount = Coins.ZERO, + forwardPayload = null, + customPayload = null + ) + + val transferDataCell = CellBuilder().storeTlb(JettonTransfer, jettonData).endCell() + + contract.transfer(pk) { + destination = MsgAddressInt.parse("kQDdAW8SkFA9Zplv-v6ysn_n4TboKLMSohBi7iipZJ3flHff") // Jetton wallet address + coins = Coins.ofNano(40000000) // Fee + messageData = MessageData.Raw(transferDataCell, null, MessageLayout.PLAIN) + } + + while (true) { + println("Wait for transaction to be processed...") + delay(6000) + val newAccountState = liteClient.getAccountState(contract.address) + if (newAccountState != accountState) { + accountState = newAccountState + println("Got new account state with last transaction: ${accountState.lastTransactionId}") + break + } + } + + val lastTransactionId = accountState.lastTransactionId + if (lastTransactionId == null) { + println("No transactions found") + return@runBlocking + } + + val transaction = liteClient.getTransactions(accountState.address, lastTransactionId, 1) + .first().transaction.value + println("Transaction: $lastTransactionId") + } +}