diff --git a/src/handlers/evm/index.ts b/src/handlers/evm/index.ts index 87d3197f..c9bc0bb4 100644 --- a/src/handlers/evm/index.ts +++ b/src/handlers/evm/index.ts @@ -20,6 +20,16 @@ export function evmHandler({ extraArgs, ); }, + async mintNft(signer, ma) { + const minter = ERC721Royalty__factory.connect(ma.contract, signer); + return minter.mint( + await signer.getAddress(), + ma.tokenId, + ma.royalty, + ma.royaltyReceiver, + ma.uri, + ); + }, getProvider() { return provider; }, diff --git a/src/handlers/evm/types.ts b/src/handlers/evm/types.ts index a6f725f2..2e515cdb 100644 --- a/src/handlers/evm/types.ts +++ b/src/handlers/evm/types.ts @@ -6,7 +6,7 @@ import { Signer, } from "ethers"; import { Bridge, BridgeStorage } from "../../contractsTypes/evm"; -import { TNftChain } from "../types"; +import { MintNft, TNftChain } from "../types"; export type TEvmHandler = TNftChain< Signer, @@ -14,7 +14,18 @@ export type TEvmHandler = TNftChain< Overrides, ContractTransactionResponse, Provider ->; +> & + MintNft< + Signer, + { + contract: string; + uri: string; + tokenId: bigint; + royalty: bigint; + royaltyReceiver: string; + }, + ContractTransactionResponse + >; export type TEvmParams = { identifier: string; diff --git a/src/handlers/multiversx/index.ts b/src/handlers/multiversx/index.ts index 90da214f..7c8e0e9f 100644 --- a/src/handlers/multiversx/index.ts +++ b/src/handlers/multiversx/index.ts @@ -103,6 +103,9 @@ export function multiversxHandler({ getStorageContract() { return storage; }, + async mintNft(_signer, _ma) { + throw new Error("unimplemented"); + }, transform(input) { return { attrs: input.metadata, diff --git a/src/handlers/multiversx/types.ts b/src/handlers/multiversx/types.ts index 8755f861..758f69ad 100644 --- a/src/handlers/multiversx/types.ts +++ b/src/handlers/multiversx/types.ts @@ -1,7 +1,7 @@ import { INetworkProvider } from "@multiversx/sdk-network-providers/out/interface"; import { UserAddress } from "@multiversx/sdk-wallet/out/userAddress"; import { BridgeStorage } from "../../contractsTypes/evm"; -import { TSingularNftChain } from "../types"; +import { MintNft, TSingularNftChain } from "../types"; // Custom Interface because there is no such signer interface in mx-sdk. export type TMultiversXSigner = { @@ -27,13 +27,27 @@ export type TMultiversXClaimArgs = { metadata: string; }; +/** + * arguments required to issue an NFT + */ +export type NftIssueArgs = { + readonly identifier: string; + readonly uris: Array; + readonly name: string; + readonly quantity?: number; + readonly royalties?: number; + readonly hash?: string; + readonly attrs?: string; +}; + export type TMultiversXHandler = TSingularNftChain< TMultiversXSigner, TMultiversXClaimArgs, unknown, string, INetworkProvider ->; +> & + MintNft; export type TMultiversXParams = { provider: INetworkProvider; diff --git a/src/handlers/secret/index.ts b/src/handlers/secret/index.ts index c9bd4fda..20f34911 100644 --- a/src/handlers/secret/index.ts +++ b/src/handlers/secret/index.ts @@ -50,6 +50,22 @@ export function secretHandler({ ); return tx; }, + mintNft(signer, ma) { + const mint = signer.tx.snip721.mint({ + contract_address: ma.contractAddress, + msg: { + mint_nft: { + public_metadata: { + token_uri: ma.uri, + }, + token_id: ma.tokenId, + owner: signer.address, + }, + }, + sender: signer.address, + }); + return mint; + }, transform(input) { return { destination_chain: input.destinationChain, diff --git a/src/handlers/secret/types.ts b/src/handlers/secret/types.ts index 269ab8e2..dbae7ef0 100644 --- a/src/handlers/secret/types.ts +++ b/src/handlers/secret/types.ts @@ -1,6 +1,6 @@ import { SecretNetworkClient, TxOptions, TxResponse } from "secretjs"; import { BridgeStorage } from "../../contractsTypes/evm"; -import { TNftChain } from "../types"; +import { MintNft, TNftChain } from "../types"; export type TSecretClaimArgs = { token_id: string; @@ -19,13 +19,20 @@ export type TSecretClaimArgs = { fee: string; }; +export type SecretMintArgs = { + contractAddress: string; + uri: string; + tokenId: string; +}; + export type TSecretHandler = TNftChain< SecretNetworkClient, TSecretClaimArgs, TxOptions, TxResponse, SecretNetworkClient ->; +> & + MintNft; export type TSecretParams = { provider: SecretNetworkClient; diff --git a/src/handlers/tezos/index.ts b/src/handlers/tezos/index.ts index 3f8bb9be..7c2fc2af 100644 --- a/src/handlers/tezos/index.ts +++ b/src/handlers/tezos/index.ts @@ -117,6 +117,21 @@ export function tezosHandler({ metadata: nft.metadata, }; }, + async mintNft(signer, ma) { + Tezos.setSignerProvider(signer); + const contract = await Tezos.contract.at(ma.contract); + const tx = contract.methods + .mint([ + { + amt: tas.nat(1), + to: tas.address(await signer.publicKeyHash()), + token_id: tas.nat(ma.tokenId), + token_uri: ma.uri, + }, + ]) + .send(); + return tx; + }, async claimNft(signer, data, extraArgs, sigs) { const isTezosAddr = validateAddress(data.source_nft_contract_address) === 3; diff --git a/src/handlers/tezos/types.ts b/src/handlers/tezos/types.ts index a741f996..5db602b0 100644 --- a/src/handlers/tezos/types.ts +++ b/src/handlers/tezos/types.ts @@ -5,9 +5,10 @@ import { } from "@taquito/taquito"; import { Signer } from "@taquito/taquito"; +import BigNumber from "bignumber.js"; import { BridgeStorage } from "../../contractsTypes/evm"; import { address, mutez, nat } from "../../contractsTypes/tezos/type-aliases"; -import { TSingularNftChain } from "../types"; +import { MintNft, TSingularNftChain } from "../types"; export type TTezosClaimArgs = { token_id: nat; @@ -26,13 +27,20 @@ export type TTezosClaimArgs = { fee: mutez; }; +export type TezosMintArgs = { + contract: string; + tokenId: BigNumber; + uri: string; +}; + export type TTezosHandler = TSingularNftChain< Signer, TTezosClaimArgs, Partial, TransactionOperation, TezosToolkit ->; +> & + MintNft; export type TTezosParams = { Tezos: TezosToolkit; diff --git a/src/handlers/ton/index.ts b/src/handlers/ton/index.ts index 9feec4f3..663455a8 100644 --- a/src/handlers/ton/index.ts +++ b/src/handlers/ton/index.ts @@ -7,6 +7,7 @@ import { } from "../../contractsTypes/ton/tonBridge"; import { NftCollection } from "../../contractsTypes/ton/tonNftCollection"; import { NftItem } from "../../contractsTypes/ton/tonNftContract"; +import { TonNftCollection } from "./nft"; import { TTonHandler, TTonParams } from "./types"; export function raise(message: string): never { @@ -249,6 +250,11 @@ export function tonHandler({ }; }, async approveNft(_signer, _tokenId, _contract) {}, + async mintNft(signer, ma) { + const nft = new TonNftCollection(ma); + await nft.deploy(signer); + return; + }, async nftData(_tokenId, contract, _overrides) { const nftItem = client.open( NftItem.fromAddress(Address.parseFriendly(contract).address), diff --git a/src/handlers/ton/nft.ts b/src/handlers/ton/nft.ts new file mode 100644 index 00000000..b8f34b8c --- /dev/null +++ b/src/handlers/ton/nft.ts @@ -0,0 +1,159 @@ +import { + Address, + Cell, + OpenedContract, + SendMode, + Sender, + StateInit, + beginCell, + contractAddress, + internal, +} from "@ton/core"; +import { KeyPair } from "@ton/crypto"; +import { WalletContractV4 } from "@ton/ton"; + +export type collectionData = { + ownerAddress: Address; + royaltyPercent: number; + royaltyAddress: Address; + nextItemIndex: number; + collectionContentUrl: string; + commonContentUrl: string; +}; + +export class TonNftCollection { + private collectionData: collectionData; + + constructor(collectionData: collectionData) { + this.collectionData = collectionData; + } + + private createCodeCell(): Cell { + const NftCollectionCodeBoc = + "te6cckECFAEAAh8AART/APSkE/S88sgLAQIBYgkCAgEgBAMAJbyC32omh9IGmf6mpqGC3oahgsQCASAIBQIBIAcGAC209H2omh9IGmf6mpqGAovgngCOAD4AsAAvtdr9qJofSBpn+pqahg2IOhph+mH/SAYQAEO4tdMe1E0PpA0z/U1NQwECRfBNDUMdQw0HHIywcBzxbMyYAgLNDwoCASAMCwA9Ra8ARwIfAFd4AYyMsFWM8WUAT6AhPLaxLMzMlx+wCAIBIA4NABs+QB0yMsCEsoHy//J0IAAtAHIyz/4KM8WyXAgyMsBE/QA9ADLAMmAE59EGOASK3wAOhpgYC42Eit8H0gGADpj+mf9qJofSBpn+pqahhBCDSenKgpQF1HFBuvgoDoQQhUZYBWuEAIZGWCqALnixJ9AQpltQnlj+WfgOeLZMAgfYBwGyi544L5cMiS4ADxgRLgAXGBEuAB8YEYGYHgAkExIREAA8jhXU1DAQNEEwyFAFzxYTyz/MzMzJ7VTgXwSED/LwACwyNAH6QDBBRMhQBc8WE8s/zMzMye1UAKY1cAPUMI43gED0lm+lII4pBqQggQD6vpPywY/egQGTIaBTJbvy9AL6ANQwIlRLMPAGI7qTAqQC3gSSbCHis+YwMlBEQxPIUAXPFhPLP8zMzMntVABgNQLTP1MTu/LhklMTugH6ANQwKBA0WfAGjhIBpENDyFAFzxYTyz/MzMzJ7VSSXwXiN0CayQ=="; + return Cell.fromBase64(NftCollectionCodeBoc); + } + + private createDataCell(): Cell { + const data = this.collectionData; + const dataCell = beginCell(); + + dataCell.storeAddress(data.ownerAddress); + dataCell.storeUint(data.nextItemIndex, 64); + const contentCell = beginCell(); + + const collectionContent = encodeOffChainContent(data.collectionContentUrl); + + const commonContent = beginCell(); + commonContent.storeBuffer(Buffer.from(data.commonContentUrl)); + + contentCell.storeRef(collectionContent); + contentCell.storeRef(commonContent.asCell()); + dataCell.storeRef(contentCell); + const NftItemCodeCell = Cell.fromBase64( + "te6cckECDQEAAdAAART/APSkE/S88sgLAQIBYgMCAAmhH5/gBQICzgcEAgEgBgUAHQDyMs/WM8WAc8WzMntVIAA7O1E0NM/+kAg10nCAJp/AfpA1DAQJBAj4DBwWW1tgAgEgCQgAET6RDBwuvLhTYALXDIhxwCSXwPg0NMDAXGwkl8D4PpA+kAx+gAxcdch+gAx+gAw8AIEs44UMGwiNFIyxwXy4ZUB+kDUMBAj8APgBtMf0z+CEF/MPRRSMLqOhzIQN14yQBPgMDQ0NTWCEC/LJqISuuMCXwSED/LwgCwoAcnCCEIt3FzUFyMv/UATPFhAkgEBwgBDIywVQB88WUAX6AhXLahLLH8s/Im6zlFjPFwGRMuIByQH7AAH2UTXHBfLhkfpAIfAB+kDSADH6AIIK+vCAG6EhlFMVoKHeItcLAcMAIJIGoZE24iDC//LhkiGOPoIQBRONkchQCc8WUAvPFnEkSRRURqBwgBDIywVQB88WUAX6AhXLahLLH8s/Im6zlFjPFwGRMuIByQH7ABBHlBAqN1viDACCAo41JvABghDVMnbbEDdEAG1xcIAQyMsFUAfPFlAF+gIVy2oSyx/LPyJus5RYzxcBkTLiAckB+wCTMDI04lUC8ANqhGIu", + ); + dataCell.storeRef(NftItemCodeCell); + const royaltyBase = 1000; + const royaltyFactor = Math.floor(data.royaltyPercent * royaltyBase); + const royaltyCell = beginCell(); + royaltyCell.storeUint(royaltyFactor, 16); + royaltyCell.storeUint(royaltyBase, 16); + royaltyCell.storeAddress(data.royaltyAddress); + dataCell.storeRef(royaltyCell); + + return dataCell.endCell(); + } + + public get stateInit(): StateInit { + const code = this.createCodeCell(); + const data = this.createDataCell(); + + return { code, data }; + } + public get address(): Address { + return contractAddress(0, this.stateInit); + } + + public async deploy(contract: Sender) { + await contract.send({ + to: this.address, + init: this.stateInit, + value: 500000n, + sendMode: SendMode.PAY_GAS_SEPARATELY + SendMode.IGNORE_ERRORS, + }); + return; + } + + public async topUpBalance( + wallet: OpenedWallet, + nftAmount: number, + ): Promise { + const feeAmount = 0.026; // approximate value of fees for 1 transaction in our case + const seqno = await wallet.contract.getSeqno(); + const amount = nftAmount * feeAmount; + + await wallet.contract.sendTransfer({ + seqno, + secretKey: wallet.keyPair.secretKey, + messages: [ + internal({ + value: amount.toString(), + to: this.address.toString({ bounceable: false }), + body: new Cell(), + }), + ], + sendMode: SendMode.PAY_GAS_SEPARATELY + SendMode.IGNORE_ERRORS, + }); + + return seqno; + } +} + +type OpenedWallet = { + contract: OpenedContract; + keyPair: KeyPair; +}; + +export function encodeOffChainContent(content: string) { + let data = Buffer.from(content); + const offChainPrefix = Buffer.from([0x01]); + data = Buffer.concat([offChainPrefix, data]); + return makeSnakeCell(data); +} +function makeSnakeCell(data: Buffer): Cell { + const chunks = bufferToChunks(data, 127); + + if (chunks.length === 0) { + return beginCell().endCell(); + } + + if (chunks.length === 1) { + return beginCell().storeBuffer(chunks[0]).endCell(); + } + + let curCell = beginCell(); + + for (let i = chunks.length - 1; i >= 0; i--) { + const chunk = chunks[i]; + + curCell.storeBuffer(chunk); + + if (i - 1 >= 0) { + const nextCell = beginCell(); + nextCell.storeRef(curCell); + curCell = nextCell; + } + } + + return curCell.endCell(); +} +function bufferToChunks(buff: Buffer, chunkSize: number) { + const chunks: Buffer[] = []; + while (buff.byteLength > 0) { + chunks.push(buff.subarray(0, chunkSize)); + //biome-ignore lint/style/noParameterAssign: weird code from ton copy pasta + buff = buff.subarray(chunkSize); + } + return chunks; +} diff --git a/src/handlers/ton/types.ts b/src/handlers/ton/types.ts index 7ef851e3..625914b3 100644 --- a/src/handlers/ton/types.ts +++ b/src/handlers/ton/types.ts @@ -2,7 +2,10 @@ import { Sender } from "@ton/core"; import { TonClient } from "@ton/ton"; import { BridgeStorage } from "../../contractsTypes/evm"; import { ClaimData } from "../../contractsTypes/ton/tonBridge"; -import { TSingularNftChain } from "../types"; +import { MintNft, TSingularNftChain } from "../types"; +import { collectionData } from "./nft"; + +export type TonMintArgs = collectionData; export type TTonHandler = TSingularNftChain< Sender, @@ -10,7 +13,8 @@ export type TTonHandler = TSingularNftChain< unknown, undefined, TonClient ->; +> & + MintNft; export type TTonParams = { client: TonClient; diff --git a/src/handlers/types/chain.ts b/src/handlers/types/chain.ts index 372b2d94..19fda267 100644 --- a/src/handlers/types/chain.ts +++ b/src/handlers/types/chain.ts @@ -1,6 +1,10 @@ import { BridgeStorage } from "../../contractsTypes/evm"; import { TSupportedChain, TSupportedSftChain } from "../../factory/types/utils"; +export type MintNft = { + mintNft(signer: Signer, ma: MintArgs): Promise; +}; + /** * Represents a function that locks an NFT on the chain inside the bridge smart contract. * @template Signer The type of the signer. ie {Signer} on EVM from ethers