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 to create jetton transfer? #44

Open
mahnunchik opened this issue Jun 21, 2024 · 31 comments
Open

How to create jetton transfer? #44

mahnunchik opened this issue Jun 21, 2024 · 31 comments

Comments

@mahnunchik
Copy link

Could you please provide sample code (in the Readme?) how to create jetton/token transfer transactio?

@vserpokryl
Copy link

vserpokryl commented Jun 27, 2024

@mahnunchik

I think this is useful for you.
Links:

import { beginCell, Address, TonClient, WalletContractV4, internal, external, storeMessage, toNano } from '@ton/ton';
import nacl from 'tweetnacl';

const apiKey = '...';
const client = new TonClient({ endpoint: 'https://toncenter.com/api/v2/jsonRPC', apiKey });

const usdtTokenContractAddress = 'EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs';
const toAddress = 'UQBcGtGIHLIQJuUHRRfWLhQxUJF4p49ywJyBdDr7UKTK60p9';

async function getUserJettonWalletAddress(userAddress: string, jettonMasterAddress: string) {
  const userAddressCell = beginCell().storeAddress(Address.parse(userAddress)).endCell();

  const response = await client.runMethod(Address.parse(jettonMasterAddress), 'get_wallet_address', [
    { type: 'slice', cell: userAddressCell },
  ]);

  return response.stack.readAddress();
}

(async () => {
  // Generate keyPair from mnemonic/secret key
  const keyPair = nacl.sign.keyPair.fromSecretKey(Buffer.from('SecretKey', 'hex'));
  const secretKey = Buffer.from(keyPair.secretKey);
  const publicKey = Buffer.from(keyPair.publicKey);

  const workchain = 0; // Usually you need a workchain 0
  const wallet = WalletContractV4.create({ workchain, publicKey });
  const address = wallet.address.toString({ urlSafe: true, bounceable: false, testOnly: false });
  const contract = client.open(wallet);

  const balance = await contract.getBalance();
  console.log({ address, balance });

  const seqno = await contract.getSeqno();
  console.log({ address, seqno });

  const { init } = contract;
  const contractDeployed = await client.isContractDeployed(Address.parse(address));
  let neededInit: null | typeof init = null;

  if (init && !contractDeployed) {
    neededInit = init;
  }

  const jettonWalletAddress = await getUserJettonWalletAddress(address, usdtTokenContractAddress);

  // Comment payload
  // const forwardPayload = beginCell()
  //   .storeUint(0, 32) // 0 opcode means we have a comment
  //   .storeStringTail('Hello, TON!')
  //   .endCell();

  const messageBody = beginCell()
    .storeUint(0x0f8a7ea5, 32) // opcode for jetton transfer
    .storeUint(0, 64) // query id
    .storeCoins(5001) // jetton amount, amount * 10^9
    .storeAddress(toAddress)
    .storeAddress(toAddress) // response destination
    .storeBit(0) // no custom payload
    .storeCoins(0) // forward amount - if > 0, will send notification message
    .storeBit(0) // we store forwardPayload as a reference, set 1 and uncomment next line for have a comment
    // .storeRef(forwardPayload)
    .endCell();

  const internalMessage = internal({
    to: jettonWalletAddress,
    value: toNano('0.1'),
    bounce: true,
    body: messageBody,
  });

  const body = wallet.createTransfer({
    seqno,
    secretKey,
    messages: [internalMessage],
  });

  const externalMessage = external({
    to: address,
    init: neededInit,
    body,
  });

  const externalMessageCell = beginCell().store(storeMessage(externalMessage)).endCell();

  const signedTransaction = externalMessageCell.toBoc();
  const hash = externalMessageCell.hash().toString('hex');

  console.log('hash:', hash);

  await client.sendFile(signedTransaction);
})();

@vserpokryl
Copy link

If you need sync method of getUserJettonWalletAddress look: https://docs.ton.org/develop/dapps/cookbook#how-to-calculate-users-jetton-wallet-address-offline

@Jobians
Copy link

Jobians commented Jul 29, 2024

@mahnunchik

I think this is useful for you. Links:

import { beginCell, Address, TonClient, WalletContractV4, internal, external, storeMessage, toNano } from '@ton/ton';
import nacl from 'tweetnacl';

const apiKey = '...';
const client = new TonClient({ endpoint: 'https://toncenter.com/api/v2/jsonRPC', apiKey });

const usdtTokenContractAddress = 'EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs';
const toAddress = 'UQBcGtGIHLIQJuUHRRfWLhQxUJF4p49ywJyBdDr7UKTK60p9';

async function getUserJettonWalletAddress(userAddress: string, jettonMasterAddress: string) {
  const userAddressCell = beginCell().storeAddress(Address.parse(userAddress)).endCell();

  const response = await client.runMethod(Address.parse(jettonMasterAddress), 'get_wallet_address', [
    { type: 'slice', cell: userAddressCell },
  ]);

  return response.stack.readAddress();
}

(async () => {
  // Generate keyPair from mnemonic/secret key
  const keyPair = nacl.sign.keyPair.fromSecretKey(Buffer.from('SecretKey', 'hex'));
  const secretKey = Buffer.from(keyPair.secretKey);
  const publicKey = Buffer.from(keyPair.publicKey);

  const workchain = 0; // Usually you need a workchain 0
  const wallet = WalletContractV4.create({ workchain, publicKey });
  const address = wallet.address.toString({ urlSafe: true, bounceable: false, testOnly: false });
  const contract = client.open(wallet);

  const balance = await contract.getBalance();
  console.log({ address, balance });

  const seqno = await contract.getSeqno();
  console.log({ address, seqno });

  const { init } = contract;
  const contractDeployed = await client.isContractDeployed(Address.parse(address));
  let neededInit: null | typeof init = null;

  if (init && !contractDeployed) {
    neededInit = init;
  }

  const jettonWalletAddress = await getUserJettonWalletAddress(address, usdtTokenContractAddress);

  // Comment payload
  // const forwardPayload = beginCell()
  //   .storeUint(0, 32) // 0 opcode means we have a comment
  //   .storeStringTail('Hello, TON!')
  //   .endCell();

  const messageBody = beginCell()
    .storeUint(0x0f8a7ea5, 32) // opcode for jetton transfer
    .storeUint(0, 64) // query id
    .storeCoins(5001) // jetton amount, amount * 10^9
    .storeAddress(toAddress)
    .storeAddress(toAddress) // response destination
    .storeBit(0) // no custom payload
    .storeCoins(0) // forward amount - if > 0, will send notification message
    .storeBit(0) // we store forwardPayload as a reference, set 1 and uncomment next line for have a comment
    // .storeRef(forwardPayload)
    .endCell();

  const internalMessage = internal({
    to: jettonWalletAddress,
    value: toNano('0.1'),
    bounce: true,
    body: messageBody,
  });

  const body = wallet.createTransfer({
    seqno,
    secretKey,
    messages: [internalMessage],
  });

  const externalMessage = external({
    to: address,
    init: neededInit,
    body,
  });

  const externalMessageCell = beginCell().store(storeMessage(externalMessage)).endCell();

  const signedTransaction = externalMessageCell.toBoc();
  const hash = externalMessageCell.hash().toString('hex');

  console.log('hash:', hash);

  await client.sendFile(signedTransaction);
})();

Error

'LITE_SERVER_UNKNOWN: cannot apply external message to current state : External message was not accepted

@Aero25x
Copy link

Aero25x commented Aug 26, 2024

If you still cant transfer jettons, here is project which do it.
https://github.com/dry-com/kozel

@LI-YONG-QI
Copy link

@vserpokryl Hello, I have a question about Jetton transfer code, why do I need to use wallet.createTransfer before external ? And where can I find more details about this ?

@vserpokryl
Copy link

@LI-YONG-QI wallet.createTransfer is body for external message in this case. You can just put createTransfer into external message body, like this:

const externalMessage = external({
  to: address,
  init: neededInit,
  body: wallet.createTransfer({
    seqno,
    secretKey,
    messages: [internalMessage],
  }),
});

@LI-YONG-QI
Copy link

LI-YONG-QI commented Aug 28, 2024

@vserpokryl Thanks for your response

Here is the source code of createWalletTransferV4 (from createTransfer)

export function createWalletTransferV4<T extends Wallet4SendArgsSignable | Wallet4SendArgsSigned>(
    args: T & { sendMode: number, walletId: number }
) {

    // Check number of messages
    if (args.messages.length > 4) {
        throw Error("Maximum number of messages in a single transfer is 4");
    }

    let signingMessage = beginCell()
        .storeUint(args.walletId, 32);
    if (args.seqno === 0) {
        for (let i = 0; i < 32; i++) {
            signingMessage.storeBit(1);
        }
    } else {
        signingMessage.storeUint(args.timeout || Math.floor(Date.now() / 1e3) + 60, 32); // Default timeout: 60 seconds
    }
    signingMessage.storeUint(args.seqno, 32);
    signingMessage.storeUint(0, 8); // Simple order
    for (let m of args.messages) {
        signingMessage.storeUint(args.sendMode, 8);
        signingMessage.storeRef(beginCell().store(storeMessageRelaxed(m)));
    }
 
    return signPayload(
        args,
        signingMessage,
        packSignatureToFront,
    ) as T extends Wallet4SendArgsSignable ? Promise<Cell> : Cell;
}

I knew the Cell was serialized by TL-B scheme in TON, but I don't know what is TL-B scheme of this Cell in createWalletTransferV4

@immujahidkhan
Copy link

import "@stdlib/deploy";
import "./google";
message MyTransfer {
amount: Int as coins;
}

contract InitiateJettonTransfer with Deployable {
masterAddress: Address;
recipientAddress: Address;
// Initialize with the master contract address, the recipient, and the amount of Jettons
init(master: Address, recipient: Address){
self.masterAddress = master;
self.recipientAddress = recipient;
}

fun getJettonWalletAddress(user: Address): Address {
    let walletInit: StateInit = initOf JettonDefaultWallet(self.masterAddress, user);
    return contractAddress(walletInit);
}

// Method to initiate the transfer of Jettons

receive(msg: MyTransfer){
    let senderWallet: Address = self.getJettonWalletAddress(sender());
    // Construct the TokenTransfer message using the provided message type
    let transferMessage: TokenTransfer = TokenTransfer{
        queryId: 0, // Set query ID to 0
        amount: msg.amount, // The amount of Jettons to transfer
        destination: myAddress(), // Destination address for Jettons
        response_destination: myAddress(), // Response destination (optional)
        custom_payload: null, // No custom payload
        forward_ton_amount: 0, // Forward TON amount (optional, set to 0)
        forward_payload: emptySlice() // Empty forward payload (optional)
    };
    // Send the transfer message to the sender's wallet
    send(SendParameters{
            to: senderWallet,
            value: ton("0.05"), // Gas fee
            bounce: true,
            body: transferMessage.toCell() // Convert the message to a Cell and send it
        }
    );
}

get fun contractBalance(): Int {
    // This should ideally return the balance of Jettons this contract has received
    return myBalance();
}

}

I tried this but my transaction still failed how to fix?
https://testnet.tonviewer.com/kQDmdvV_XTLbQWFJTJShJE36zPlbJHxlURoPXImn0xyyN6hw

@Omokami
Copy link

Omokami commented Oct 25, 2024

When you run the above code, the following error occurs.

contractDeployed false;
POST https://toncenter.com/api/v2/jsonRPC 500 (Internal Server Error)

Can you help me?
image

@ilaziness
Copy link

@Omokami
Copy link

Omokami commented Nov 6, 2024

@vserpokryl

I solved the problem

Thank you

@Tvenus
Copy link

Tvenus commented Nov 8, 2024

@Omokami

You are great!!!

@Omokami
Copy link

Omokami commented Nov 8, 2024

@vserpokryl

Hello

I need urgent help on how to import my Jetton transaction history

@vserpokryl
Copy link

@Omokami, hello! You can use https://tonapi.io for this. For example getAccountJettonsHistory - https://tonapi.io/api-v2#operations-Accounts-getAccountJettonsHistory

@Omokami
Copy link

Omokami commented Nov 8, 2024

@vserpokryl

Thank you

but, I would appreciate it if you could explain how to get the account ID and jetton ID.

@vserpokryl
Copy link

@Omokami
account_id - your ton address
jetton_id - contract address (jetton master address) of token (Jetton) (for example USDT - EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs)

If you need all history of jetton transfers for all tokens use getAccountJettonsHistory (/v2/accounts/{account_id}/jettons/history)
If you need history for specific Jetton (USDT for example) use getAccountJettonHistoryByID (/v2/accounts/{account_id}/jettons/{jetton_id}/history)

@Omokami
Copy link

Omokami commented Nov 8, 2024

@vserpokryl
Copy link

@Omokami
Check the specifications here https://tonapi.io/api-v2#operations-Accounts-getAccountJettonsHistory (expand the route and check details)
https://tonapi.io/v2/accounts/EQAZ3iBMMvTmQmBSJWtQW6ISeu9Qmiiqs2cq_aionprmhXfw/jettons/history?limit=100

@Omokami
Copy link

Omokami commented Nov 8, 2024

@vserpokryl

Thank you very much

@Omokami
Copy link

Omokami commented Nov 19, 2024

@vserpokryl

Hi
How are you doing?

Is there a fee of 0.12 when transferring Jetton?
To send TON is only 0.01 TON for fees.
I would appreciate it if you could tell me how the fees are determined.

I 'll wait for you reply

@vserpokryl
Copy link

@Omokami
Hi!
I don’t have a simple answer for you. The fee calculation in the TON network is quite complex, and we also face challenges when estimating fees. I recommend checking the documentation on this topic:

@Omokami
Copy link

Omokami commented Nov 19, 2024

@vserpokryl
Hello
What is the standard for distinguishing between what was sent and what was received in transaction data obtained using the API?

@IlyaSemenov
Copy link

FWIW, here's my code for preparing an USDT transfer tx using the latest versions of the tooling:

import { Buffer } from "node:buffer"

import { randomBytes } from "@noble/hashes/utils"
import { Address, beginCell, comment, toNano } from "@ton/core"
import { JettonMaster } from "@ton/ton"
import { TonApiClient } from "@ton-api/client"
import { ContractAdapter } from "@ton-api/ton-adapter"
import { storeJettonTransferMessage } from "@ton-community/assets-sdk"
import { CHAIN, type SendTransactionRequest } from "@tonconnect/sdk"

const tonApi = new TonApiClient({
  baseUrl: "https://tonapi.io",
  apiKey: "...",
})
const adapter = new ContractAdapter(tonApi)

export async function createUsdtTxRequest(
  { sender, recipient, amount, comment: commentText }:
  { sender: string, recipient: string, amount: number, comment: string },
): Promise<SendTransactionRequest> {
  const senderAddress = Address.parse(sender)

  // https://docs.ton.org/v3/guidelines/dapps/cookbook#how-to-construct-a-message-for-a-jetton-transfer-with-a-comment
  // https://github.com/ton-org/ton/issues/44
  const messageCell = beginCell()
    .store(storeJettonTransferMessage({
      queryId: createJettonTransferQueryId(),
      amount: toRawUsdt(amount),
      destination: Address.parse(recipient),
      // Return excessive TON back to sender.
      responseDestination: senderAddress,
      customPayload: null,
      // Pay for forward payload.
      forwardAmount: 1n,
      forwardPayload: comment(commentText),
    }))
    .endCell()

  const senderUsdtWalletAddress = await getUsdtWalletAddress(senderAddress)

  return {
    network: CHAIN.MAINNET,
    messages: [{
      address: senderUsdtWalletAddress.toString(),
      // Make sure we have enough TON for commission. Extra will be returned back to sender.
      amount: toNano("0.1").toString(),
      payload: messageCell.toBoc().toString("base64"),
    }],
    validUntil: Math.floor(Date.now() / 1000) + 60, // 60 sec
  }
}

function bigintFromUint8Array(arr: Uint8Array) {
  return BigInt("0x" + Buffer.from(arr).toString("hex"))
}

/**
 * Generate Jetton transfer query ID. Must be unique u64.
 */
function createJettonTransferQueryId() {
  return bigintFromUint8Array(randomBytes(8))
}

async function getJettonWalletAddress(jettonAddress: Address, userAddress: Address): Promise<Address> {
  const jettonMaster = adapter.open(JettonMaster.create(jettonAddress))
  const userJettonAddress = await jettonMaster.getWalletAddress(userAddress)
  return userJettonAddress
}

// Well-known address for USDT on TON.
const usdtContractAddress = Address.parse("EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs")
const usdtDecimals = 6

function getUsdtWalletAddress(userAddress: Address) {
  return getJettonWalletAddress(usdtContractAddress, userAddress)
}

function toRawUsdt(amount: number) {
  return BigInt(Math.round(amount * 10 ** usdtDecimals))
}

@newstable
Copy link

newstable commented Dec 13, 2024

Hello. I'm gonna make the transfer USDT function using my mnemonic in node.js.

@vserpokryl. I tried to use your above example code but I don't get any issues and any result.

const USDT_MASTER_ADDRESS = Address.parse('kQAau4V6DsCRy_fmfDymJO5H_kLnIkjjKYLkXbBM0PZ1rtZy')
const toAddress = '0QD5UUpPBopK-LayGy1N8U2JpqbYaSGGfcSDFhvt8qpY-1A2'

const client = new TonClient({
    endpoint: 'https://testnet.toncenter.com/api/v2/jsonRPC',
    apiKey: 'd88e64278efdddc0395e494a90a8938ef0541c91cc3766ba96dd355861ed17db'
})

export const SendUSDT = async () => {
    try {
        const mnemonic = process.env.ADMIN_MNEMONIC
        const seed = await mnemonicToSeed(
            'truly gift exit undo limit happy found coyote rookie deputy chicken movie pave avoid average box magnet tide orbit process gentle metal name loan'
        )
        const hashedSeed = sha256.array(seed)
        const usdtTokenContractAddress = 'kQAau4V6DsCRy_fmfDymJO5H_kLnIkjjKYLkXbBM0PZ1rtZy'

        const keyPair = nacl.sign.keyPair.fromSeed(new Uint8Array(hashedSeed.slice(0, 32)))

        const secretKey = Buffer.from(keyPair.secretKey)
        const publicKey = Buffer.from(keyPair.publicKey)

        const workchain = 0
        const wallet = WalletContractV4.create({ workchain, publicKey })
        const address = wallet.address.toString({ urlSafe: true, bounceable: false, testOnly: false })
        console.log('Wallet Address:', address)
        const contract = client.open(wallet)
        const balance = await contract.getBalance()
        console.log('Ton Balance:', balance)

        const seqno = await contract.getSeqno()

        const { init } = contract
        const contractDeployed = await client.isContractDeployed(Address.parse(address))
        let neededInit: null | typeof init = null

        if (init && !contractDeployed) {
            neededInit = init
        }

        const jettonWalletAddress = await getUserJettonWalletAddress(address, usdtTokenContractAddress)
        console.log('jettonWalletAddress:', jettonWalletAddress)
        const usdtBalance = await getUSDTBalance(jettonWalletAddress)

        console.log('USDT Balance:', usdtBalance.toString())

        const messageBody = beginCell()
            .storeUint(0x0f8a7ea5, 32)
            .storeUint(0, 64)
            .storeCoins(5001)
            .storeAddress(Address.parse(toAddress))
            .storeAddress(Address.parse(toAddress))
            .storeBit(0)
            .storeCoins(0)
            .storeBit(0)
            .endCell()

        const internalMessage = internal({
            to: jettonWalletAddress,
            value: toNano('1'),
            bounce: true,
            body: messageBody
        })

        const body = wallet.createTransfer({
            seqno,
            secretKey,
            messages: [internalMessage]
        })

        const externalMessage = external({
            to: address,
            init: neededInit,
            body
        })

        const externalMessageCell = beginCell().store(storeMessage(externalMessage)).endCell()

        const signedTransaction = externalMessageCell.toBoc()
        const hash = externalMessageCell.hash().toString('hex')

        console.log('hash:', hash)

        await client.sendFile(signedTransaction)
        console.log(`Sent 1 nanoTONs to 0QD5UUpPBopK-LayGy1N8U2JpqbYaSGGfcSDFhvt8qpY-1A2`)
    } catch (error: any) {
        console.log(error.message)
    }
}

const getUserJettonWalletAddress = async (userAddress: string, jettonMasterAddress: string) => {
    const userAddressCell = beginCell().storeAddress(Address.parse(userAddress)).endCell()

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

    return response.stack.readAddress()
}

const getUSDTBalance = async (jettonWalletAddress: Address) => {
    const response = await client.runMethod(USDT_MASTER_ADDRESS, 'get_balance', [
        { type: 'slice', cell: beginCell().storeAddress(jettonWalletAddress).endCell() }
    ])
    console.log('Response:', response)
    return response.stack.readBigNumber() // Return the balance as a BigNumber
}

SendUSDT()

Please help me to complete this funciton

@Tvenus
Copy link

Tvenus commented Dec 13, 2024

@newstable

I have experienced Jetton Wallet development, token automatic systems, and other experiences.

maybe, I can help you

@newstable
Copy link

newstable commented Dec 13, 2024

Hi @Tvenus Nice to meet you.
Now I'm trying to test on testnet.
This is my jettonWalletAddress : EQD7H6nQaN2PuLOckmZ1nFShJDubqOqLLK9RnEHEBFcD4JWN
And this is toAddress : 0QD5UUpPBopK-LayGy1N8U2JpqbYaSGGfcSDFhvt8qpY-1A2

const messageBody = beginCell()
            .storeUint(0x0f8a7ea5, 32)
            .storeUint(0, 64)
            .storeCoins(5001)
            .storeAddress(Address.parse(toAddress))
            .storeAddress(Address.parse(toAddress))
            .storeBit(0)
            .storeCoins(0)
            .storeBit(0)
            .endCell()

        const internalMessage = internal({
            to: jettonWalletAddress,
            value: toNano('1'),
            bounce: true,
            body: messageBody
        })

        const body = wallet.createTransfer({
            seqno,
            secretKey,
            messages: [internalMessage]
        })

        const externalMessage = external({
            to: address,
            init: neededInit,
            body
        })

        const externalMessageCell = beginCell().store(storeMessage(externalMessage)).endCell()

        const signedTransaction = externalMessageCell.toBoc()
        const hash = externalMessageCell.hash().toString('hex')

        console.log('hash:', hash)

        await client.sendFile(signedTransaction)

And I'm trying to send test 1 USDT.

This is my test USDT Master Address : kQAau4V6DsCRy_fmfDymJO5H_kLnIkjjKYLkXbBM0PZ1rtZy

I don't understand the difference between jetton address and wallet address.

It seems like difference between wallet address and token account address in solana.

Please let me know what did I make mistake in my code.

@Tvenus
Copy link

Tvenus commented Dec 13, 2024

const messageBody = beginCell()
.storeUint(0x0f8a7ea5, 32)
.storeUint(0, 64)
.storeCoins(5001) //Enter Jetton number instead of 5001.
.storeAddress(Address.parse(toAddress))
.storeAddress(Address.parse(toAddress))
.storeBit(0)
.storeCoins(0)
.storeBit(0)
.endCell()

    const internalMessage = internal({
        to: jettonWalletAddress,
        value: toNano('1'),                                               //The amount of fee for sending Jetton
        bounce: true,
        body: messageBody
    })

    const body = wallet.createTransfer({
        seqno,
        secretKey,
        messages: [internalMessage]
    })

    const externalMessage = external({
        to: address,
        init: neededInit,
        body
    })

    const externalMessageCell = beginCell().store(storeMessage(externalMessage)).endCell()

    const signedTransaction = externalMessageCell.toBoc()
    const hash = externalMessageCell.hash().toString('hex')

    console.log('hash:', hash)

    await client.sendFile(signedTransaction)

@newstable
Copy link

newstable commented Dec 13, 2024

So is it because I entered the jetton number incorrectly?

If then, where can I find the jetton number?

@Tvenus
Copy link

Tvenus commented Dec 13, 2024

//Enter Jetton value instead of 5001.

@newstable
Copy link

Thank you @Tvenus 💕

It's working well.

@Tvenus
Copy link

Tvenus commented Dec 13, 2024

@newstable

I am very interested in your work, and I wanna work with you.
Could you please give me an opportunity to work with you?

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