Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How can I send a message to my contract and send jetton to that contract in a single transaction? #168

Open
0xBeycan opened this issue Jan 17, 2025 · 1 comment

Comments

@0xBeycan
Copy link

When I try as below and try to create the messages separately and send them in a single createTransfer, I get an 'invalid address' warning. But when I send the messages separately, there is no problem.

import TonCenterV3 from './v3/index'
import { mnemonicToPrivateKey } from '@ton/crypto'
import { TonClient, WalletContractV4 } from '@ton/ton'
import { TokenICO as BaseTokenICO, storePublish } from '../build/TokenICO/tact_TokenICO'
import { Address, beginCell, Cell, internal, MessageRelaxed, toNano, SendMode } from '@ton/core'

export * as Base from '../build/TokenICO/tact_TokenICO'

export interface Presale {
    tokenAddress: string
    totalSaleLimit: number
    minContribution: number
    maxContribution: number
    exchangeRate: number
    startTime: number
    endTime: number
    instantTransfer: boolean
}

export interface Config {
    testnet?: boolean
    apiKey: string
}

export interface TokenMetadata {
    name: string
    symbol: string
    decimals: number
    description: string
    image: string
}

export default class TokenICO {
    config: Config

    base: BaseTokenICO

    client: TonClient

    client3: TonCenterV3

    mainnetEndpoint = 'https://toncenter.com/api/v2/jsonRPC'

    testnetEndpoint = 'https://testnet.toncenter.com/api/v2/jsonRPC'

    constructor(address: string, config: Config) {
        this.config = config
        this.client3 = new TonCenterV3(config)
        this.base = BaseTokenICO.fromAddress(Address.parse(address))
        this.client = new TonClient({
            endpoint: config.testnet ? this.testnetEndpoint : this.mainnetEndpoint,
            apiKey: config.apiKey,
        })
    }

    getAddress(): Address {
        return this.base.address
    }

    getStringAddress(): string {
        return this.base.address.toString({
            testOnly: this.config.testnet,
            bounceable: true,
        })
    }

    formatPresaleKey(presaleKey: string): bigint {
        return BigInt('0x' + Buffer.from(presaleKey, 'utf8').toString('hex'))
    }

    async getTokenMetadata(tokenAddress: string): Promise<TokenMetadata> {
        const response = await this.client3.getJettonMasters({
            address: [tokenAddress],
        })
        return response.jetton_masters[0]?.jetton_content as any as TokenMetadata
    }

    async getJettonWalletAddress(tokenAddress: string, ownerAddress: string): Promise<Address> {
        const contractCell = beginCell().storeAddress(Address.parse(ownerAddress)).endCell()

        const response = await this.client.runMethod(Address.parse(tokenAddress), 'get_wallet_address', [
            { type: 'slice', cell: contractCell },
        ])

        return response.stack.readAddress()
    }

    async createPublishPresaleCell(presaleKey: bigint, presale: Presale): Promise<Cell> {
        const tokenData = await this.getTokenMetadata(presale.tokenAddress)

        const jettonWallet = (await this.getJettonWalletAddress(
            presale.tokenAddress,
            this.getStringAddress(),
        )).toString({
            testOnly: this.config.testnet,
            bounceable: true,
        })

        return beginCell()
            .store(
                storePublish({
                    presaleKey,
                    $$type: 'Publish',
                    jettonWallet: Address.parse(jettonWallet),
                    tokenAddress: Address.parse(presale.tokenAddress),
                    totalSaleLimit: toNano(presale.totalSaleLimit),
                    minContribution: toNano(presale.minContribution),
                    maxContribution: toNano(presale.maxContribution),
                    exchangeRate: BigInt(presale.exchangeRate),
                    startTime: BigInt(presale.startTime),
                    endTime: BigInt(presale.endTime),
                    instantTransfer: presale.instantTransfer,
                    decimals: BigInt(tokenData.decimals),
                }),
            )
            .endCell()
    }

    async createPublishPresaleMessage(presaleKey: bigint, amount: number, presale: Presale): Promise<MessageRelaxed> {
        const cell = await this.createPublishPresaleCell(presaleKey, presale)

        return internal({
            body: cell,
            bounce: true,
            to: this.base.address,
            value: toNano(amount + 0.05),
        })
    }

    async createTokenTransferMessage(
        token: string,
        sender: string,
        receiver: string,
        amount: number,
    ): Promise<MessageRelaxed> {
        const tokenData = await this.getTokenMetadata(token)
        const formattedAmount = amount * 10 ** tokenData.decimals

        const senderJettonWallet = await this.getJettonWalletAddress(token, sender)

        const cell = beginCell()
            .storeUint(0xf8a7ea5, 32)
            .storeUint(0, 64)
            .storeCoins(formattedAmount)
            .storeAddress(Address.parse(receiver))
            .storeAddress(Address.parse(sender))
            .storeBit(0)
            .storeCoins(1n)
            .storeBit(1)
            .storeRef(beginCell().storeUint(0, 32).storeStringTail('ico-token-deposit').endCell())
            .endCell()

        return internal({
            to: senderJettonWallet,
            value: toNano('0.05'),
            bounce: true,
            body: cell
        })
    }

    async retry<T>(fn: () => Promise<T>, options: { retries: number; delay: number }): Promise<T> {
        let lastError: Error | undefined
        for (let i = 0; i < options.retries; i++) {
            try {
                return await fn()
            } catch (e) {
                if (e instanceof Error) {
                    lastError = e
                }
                await new Promise((resolve) => setTimeout(resolve, options.delay))
            }
        }
        throw lastError ?? new Error('Unknown error')
    }

    async findTxHashByBodyHash(_hash: string): Promise<string> {
        return await this.retry(
            async () => {
                const { messages } = await this.client3.getMessages({
                    body_hash: _hash,
                })
                if (messages[0]) {
                    return Buffer.from(messages[0].in_msg_tx_hash, 'base64').toString('hex')
                } else {
                    throw new Error('Transaction not found')
                }
            },
            { retries: 30, delay: 1000 },
        )
    }

    async sendMessages(messages: MessageRelaxed[], privateKey: string): Promise<string> {
        const { publicKey, secretKey } = await mnemonicToPrivateKey(privateKey.split(' '))
        const contract = WalletContractV4.create({ workchain: 0, publicKey })
        const wallet = this.client.open(contract)
        const seqno = await wallet.getSeqno()
        const signedData = wallet.createTransfer({
            seqno,
            secretKey,
            messages,
            sendMode: SendMode.PAY_GAS_SEPARATELY,
        })
        await wallet.send(signedData)
        return await this.findTxHashByBodyHash(signedData.hash().toString('hex'))
    }
}

Using file: publishPresale2 Connected to wallet at address: EQBg0oduUuIGOkdzpWwVzFaHJqIdDDJ8ylgQxAD8VZRXzQtF Error: Invalid address. Got EQCkRxH16paRh_27zaTflO_BwJoARIQ1-3d_1tHlBIbV7RIa at BitBuilder.writeAddress (/Users/beycan/Desktop/tokenico/tokenico-ts/contracts/ton/node_modules/.pnpm/@[email protected]_@[email protected]/node_modules/@ton/core/dist/boc/BitBuilder.js:256:15) at Builder.storeAddress (/Users/beycan/Desktop/tokenico/tokenico-ts/contracts/ton/node_modules/.pnpm/@[email protected]_@[email protected]/node_modules/@ton/core/dist/boc/Builder.js:237:20) at /Users/beycan/Desktop/tokenico/tokenico-ts/contracts/ton/node_modules/.pnpm/@[email protected]_@[email protected]/node_modules/@ton/core/dist/types/CommonMessageInfoRelaxed.js:65:21 at Builder.storeWritable (/Users/beycan/Desktop/tokenico/tokenico-ts/contracts/ton/node_modules/.pnpm/@[email protected]_@[email protected]/node_modules/@ton/core/dist/boc/Builder.js:338:13) at Builder.store (/Users/beycan/Desktop/tokenico/tokenico-ts/contracts/ton/node_modules/.pnpm/@[email protected]_@[email protected]/node_modules/@ton/core/dist/boc/Builder.js:362:14) at /Users/beycan/Desktop/tokenico/tokenico-ts/contracts/ton/node_modules/.pnpm/@[email protected]_@[email protected]/node_modules/@ton/core/dist/types/MessageRelaxed.js:36:17 at Builder.storeWritable (/Users/beycan/Desktop/tokenico/tokenico-ts/contracts/ton/node_modules/.pnpm/@[email protected]_@[email protected]/node_modules/@ton/core/dist/boc/Builder.js:338:13) at Builder.store (/Users/beycan/Desktop/tokenico/tokenico-ts/contracts/ton/node_modules/.pnpm/@[email protected]_@[email protected]/node_modules/@ton/core/dist/boc/Builder.js:362:14) at createWalletTransferV4 (/Users/beycan/Desktop/tokenico/tokenico-ts/contracts/ton/node_modules/.pnpm/@[email protected]_@[email protected]_@[email protected]__@[email protected]/node_modules/@ton/ton/dist/wallets/signing/createWalletTransfer.js:125:57) at Proxy.createTransfer (/Users/beycan/Desktop/tokenico/tokenico-ts/contracts/ton/node_modules/.pnpm/@[email protected]_@[email protected]_@[email protected]__@[email protected]/node_modules/@ton/ton/dist/wallets/WalletContractV4.js:75:66)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants
@0xBeycan and others