diff --git a/package.json b/package.json index a50c316..642780b 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "ton-crypto": "^3.2.0", "ton-emulator": "^1.2.0", "ton-nodejs": "^1.4.3", - "ton-tact": "^0.5.0", + "ton-tact": "^0.8.7", "ts-jest": "^29.0.3", "ts-node": "^10.9.1", "typescript": "^4.9.4" diff --git a/sources/jetton.deploy.ts b/sources/jetton.deploy.ts index 8e8fe5f..70e7da0 100644 --- a/sources/jetton.deploy.ts +++ b/sources/jetton.deploy.ts @@ -1,20 +1,204 @@ -import { contractAddress, toNano } from "ton"; -import { packAdd, SampleTactContract_init } from "./output/sample_SampleTactContract"; +import { beginCell, contractAddress, toNano, TonClient, TonClient4, Address, WalletContractV4, internal, fromNano} from "ton"; +import {mnemonicToPrivateKey} from "ton-crypto"; +import { packAdd, init } from "./output/jetton_SampleJetton"; import { printAddress, printDeploy, printHeader } from "./utils/print"; import { randomAddress } from "./utils/randomAddress"; +import {SampleJetton_errors} from "./output/jetton_SampleJetton"; -(async () => { //need to changes for jetton - - // Parameters - // let owner = randomAddress(0, 'some-owner'); // Replace owner with your address - // let packed = packAdd({ $$type: 'Add', amount: 10n }); // Replace if you want another message used - // let init = await SampleTactContract_init(owner); - // let address = contractAddress({ workchain: 0, initialCode: init.code, initialData: init.data }); - // let deployAmount = toNano(10); - // let testnet = true; - // - // // Print basics - // printHeader('SampleTactContract'); - // printAddress(address); - // printDeploy(init, deployAmount, packed, testnet); -})(); \ No newline at end of file +(async () => { //need changes for jetton + + //create client for testnet Toncenter API + const client = new TonClient({ + endpoint: 'https://testnet.toncenter.com/api/v2/jsonRPC', + apiKey: 'bb38df0c2756c66e2ab49f064e2484ec444b01244d2bd49793bd5b58f61ae3d2' + }) + + //create client for testnet sandboxv4 API - alternative endpoint + const client4 = new TonClient4({ + endpoint: "https://sandbox-v4.tonhubapi.com" + }) + + // Insert your test wallet's 24 words, make sure you have some test Toncoins on its balance. Every deployment spent 0.5 test toncoin. + let mnemonics = "multiply voice predict admit hockey fringe flat bike napkin child quote piano year cloud bundle lunch...."; + // read more about wallet apps https://ton.org/docs/participate/wallets/apps#tonhub-test-environment + + let keyPair = await mnemonicToPrivateKey(mnemonics.split(" ")); + let secretKey = keyPair.secretKey; + //workchain = 1 - masterchain (expensive operation cost, validator's election contract works here) + //workchain = 0 - basechain (normal operation cost, user's contracts works here) + let workchain = 0; //we are working in basechain. + + //Create deployment wallet contract + let wallet = WalletContractV4.create({ workchain, publicKey: keyPair.publicKey}); + let contract = client.open(wallet); + + // Get deployment wallet balance + let balance: bigint = await contract.getBalance(); + + + // Generate define owner of Jetton contract + let owner = Address.parse('kQDND6yHEzKB82ZGRn58aY9Tt_69Ie_uz73e2VuuJ3fVVcxf'); + + // Create content Cell + let content = beginCell().storeUint() + + // Compute init for deployment + let init = await SampleJetton.init(owner, content); + + // send a message on new address contract to deploy it + let seqno: number = await contract.getSeqno(); + console.log('🛠️Preparing new outgoing massage from deployment wallet. Seqno = ', seqno); + console.log('Current deployment wallet balance = ', fromNano(balance).toString(), '💎TON'); + await contract.sendTransfer({ + seqno, + secretKey, + messages: [internal({ + value: deployAmount, + to: destination_address, + init: { + code : init.code, + data : init.data + }, + body: 'Deploy' + })] + }); + console.log('======deployment message sent to ', destination_address, ' ======'); +})(); + + +export type JettonMetaDataKeys = "name" | "description" | "image" | "symbol"; + +const jettonOnChainMetadataSpec: { + [key in JettonMetaDataKeys]: "utf8" | "ascii" | undefined; +} = { + name: "utf8", + description: "utf8", + image: "ascii", + symbol: "utf8", +}; + +const sha256 = (str: string) => { + const sha = new Sha256(); + sha.update(str); + return Buffer.from(sha.digestSync()); +}; + +export function buildTokenMetadataCell(data: { [s: string]: string | undefined }): Cell { + const KEYLEN = 256; + const dict = beginDict(KEYLEN); + + Object.entries(data).forEach(([k, v]: [string, string | undefined]) => { + if (!jettonOnChainMetadataSpec[k as JettonMetaDataKeys]) + throw new Error(`Unsupported onchain key: ${k}`); + if (v === undefined || v === "") return; + + let bufferToStore = Buffer.from(v, jettonOnChainMetadataSpec[k as JettonMetaDataKeys]); + + const CELL_MAX_SIZE_BYTES = Math.floor((1023 - 8) / 8); + + const rootCell = new Cell(); + rootCell.bits.writeUint8(SNAKE_PREFIX); + let currentCell = rootCell; + + while (bufferToStore.length > 0) { + currentCell.bits.writeBuffer(bufferToStore.slice(0, CELL_MAX_SIZE_BYTES)); + bufferToStore = bufferToStore.slice(CELL_MAX_SIZE_BYTES); + if (bufferToStore.length > 0) { + const newCell = new Cell(); + currentCell.refs.push(newCell); + currentCell = newCell; + } + } + + dict.storeRef(sha256(k), rootCell); + }); + + return beginCell().storeInt(ONCHAIN_CONTENT_PREFIX, 8).storeDict(dict.endDict()).endCell(); +} + +export function parseTokenMetadataCell(contentCell: Cell): { + [s in JettonMetaDataKeys]?: string; +} { + // Note that this relies on what is (perhaps) an internal implementation detail: + // "ton" library dict parser converts: key (provided as buffer) => BN(base10) + // and upon parsing, it reads it back to a BN(base10) + // tl;dr if we want to read the map back to a JSON with string keys, we have to convert BN(10) back to hex + const toKey = (str: string) => new BN(str, "hex").toString(10); + + const KEYLEN = 256; + const contentSlice = contentCell.beginParse(); + if (contentSlice.readUint(8).toNumber() !== ONCHAIN_CONTENT_PREFIX) + throw new Error("Expected onchain content marker"); + + const dict = contentSlice.readDict(KEYLEN, (s) => { + const buffer = Buffer.from(""); + + const sliceToVal = (s: Slice, v: Buffer, isFirst: boolean) => { + s.toCell().beginParse(); + if (isFirst && s.readUint(8).toNumber() !== SNAKE_PREFIX) + throw new Error("Only snake format is supported"); + + v = Buffer.concat([v, s.readRemainingBytes()]); + if (s.remainingRefs === 1) { + v = sliceToVal(s.readRef(), v, false); + } + + return v; + }; + + return sliceToVal(s.readRef(), buffer, true); + }); + + const res: { [s in JettonMetaDataKeys]?: string } = {}; + + Object.keys(jettonOnChainMetadataSpec).forEach((k) => { + const val = dict + .get(toKey(sha256(k).toString("hex"))) + ?.toString(jettonOnChainMetadataSpec[k as JettonMetaDataKeys]); + if (val) res[k as JettonMetaDataKeys] = val; + }); + + return res; +} + +export function jettonMinterInitData( + owner: Address, + metadata: { [s in JettonMetaDataKeys]?: string } +): Cell { + return beginCell() + .storeCoins(0) + .storeAddress(owner) + .storeRef(buildTokenMetadataCell(metadata)) + .storeRef(JETTON_WALLET_CODE) + .endCell(); +} + +// return the init Cell of the contract storage (according to load_data() contract method) +export function initData() { + return jettonMinterInitData(jettonParams.owner, { + name: jettonParams.name, + symbol: jettonParams.symbol, + image: jettonParams.image, + description: jettonParams.description, + }); +} + +// return the op that should be sent to the contract on deployment, can be "null" to send an empty message +export function initMessage() { + return null; // TODO? +} + +// optional end-to-end sanity test for the actual on-chain contract to see it is actually working on-chain +export async function postDeployTest( + walletContract: WalletContract, + secretKey: Buffer, + contractAddress: Address +) { + const call = await walletContract.client.callGetMethod(contractAddress, "get_jetton_data"); + + console.log( + parseTokenMetadataCell( + Cell.fromBoc(Buffer.from(call.stack[3][1].bytes, "base64").toString("hex"))[0] + ) + ); +} diff --git a/sources/jetton.spec.ts b/sources/jetton.spec.ts index 915f975..6823c85 100644 --- a/sources/jetton.spec.ts +++ b/sources/jetton.spec.ts @@ -1,4 +1,4 @@ -import { toNano } from "ton-core"; +import { toNano, beginCell } from "ton-core"; import { ContractSystem } from "ton-emulator"; import {SampleJetton, SampleJetton_init} from './output/jetton_SampleJetton'; @@ -8,6 +8,9 @@ describe('jetton', () => { // Create jetton let system = await ContractSystem.create(); let owner = system.treasure('owner'); + + let content = beginCell().storeUint(1, 16).storeSlice('fff').endCell(); + let contract = system.open(await SampleJetton.fromInit(owner.address, null)); let tracker = system.track(contract.address); diff --git a/sources/jetton.tact b/sources/jetton.tact index f7d289e..b41e060 100644 --- a/sources/jetton.tact +++ b/sources/jetton.tact @@ -6,6 +6,8 @@ message Mint { contract SampleJetton with Jetton { +// storage#_ balance:Grams owner_address:MsgAddressInt jetton_master_address:MsgAddressInt jetton_wallet_code:^Cell = Storage + totalSupply: Int as coins; owner: Address; content: Cell?; diff --git a/sources/utils/jetton-helpers.ts b/sources/utils/jetton-helpers.ts new file mode 100644 index 0000000..c4a5675 --- /dev/null +++ b/sources/utils/jetton-helpers.ts @@ -0,0 +1,37 @@ +import { Cell, beginCell, Address, beginDict, Slice, toNano } from "ton"; + +let contentSlice2 : Slice; + + + +enum OPS { + ChangeAdmin = 3, + ReplaceMetadata = 4, + Mint = 21, + InternalTransfer = 0x178d4519, + Transfer = 0xf8a7ea5, + Burn = 0x595f07bc, +} + +export type JettonMetaDataKeys = + | "name" + | "description" + | "image" + | "symbol" + | "image_data" + | "decimals"; + +async function parseJettonOffchainMetadata(contentSlice: Slice): Promise<{ + metadata: { [s in JettonMetaDataKeys]?: string }; + isIpfs: boolean; +}> { + const jsonURI = contentSlice + .loadBits(await () => (contentSlice.remainingBits())) + .toString("ascii") + .replace("ipfs://", "https://ipfs.io/ipfs/"); + + return { + metadata: (await axios.get(jsonURI)).data, + isIpfs: /(^|\/)ipfs[.:]/.test(jsonURI), + }; +} \ No newline at end of file