wormhole/cosmwasm/contracts/cw721-base/helpers.ts

394 lines
14 KiB
TypeScript

import axios from "axios";
import fs from "fs";
import { SigningCosmWasmClient } from "@cosmjs/cosmwasm-stargate";
import { GasPrice, calculateFee, StdFee } from "@cosmjs/stargate";
import { DirectSecp256k1HdWallet, makeCosmoshubPath } from "@cosmjs/proto-signing";
import { Slip10RawIndex } from "@cosmjs/crypto";
import path from "path";
/*
* This is a set of helpers meant for use with @cosmjs/cli
* With these you can easily use the cw721 contract without worrying about forming messages and parsing queries.
*
* Usage: npx @cosmjs/cli@^0.26 --init https://raw.githubusercontent.com/CosmWasm/cosmwasm-plus/master/contracts/cw721-base/helpers.ts
*
* Create a client:
* const [addr, client] = await useOptions(pebblenetOptions).setup('password');
*
* Get the mnemonic:
* await useOptions(pebblenetOptions).recoverMnemonic(password);
*
* Create contract:
* const contract = CW721(client, pebblenetOptions.fees);
*
* Upload contract:
* const codeId = await contract.upload(addr);
*
* Instantiate contract example:
* const initMsg = {
* name: "Potato Coin",
* symbol: "TATER",
* minter: addr
* };
* const instance = await contract.instantiate(addr, codeId, initMsg, 'Potato Coin!');
* If you want to use this code inside an app, you will need several imports from https://github.com/CosmWasm/cosmjs
*/
interface Options {
readonly httpUrl: string
readonly networkId: string
readonly feeToken: string
readonly bech32prefix: string
readonly hdPath: readonly Slip10RawIndex[]
readonly faucetUrl?: string
readonly defaultKeyFile: string,
readonly fees: {
upload: StdFee,
init: StdFee,
exec: StdFee
}
}
const pebblenetGasPrice = GasPrice.fromString("0.01upebble");
const pebblenetOptions: Options = {
httpUrl: 'https://rpc.pebblenet.cosmwasm.com',
networkId: 'pebblenet-1',
bech32prefix: 'wasm',
feeToken: 'upebble',
faucetUrl: 'https://faucet.pebblenet.cosmwasm.com/credit',
hdPath: makeCosmoshubPath(0),
defaultKeyFile: path.join(process.env.HOME, ".pebblenet.key"),
fees: {
upload: calculateFee(1500000, pebblenetGasPrice),
init: calculateFee(500000, pebblenetGasPrice),
exec: calculateFee(200000, pebblenetGasPrice),
},
}
interface Network {
setup: (password: string, filename?: string) => Promise<[string, SigningCosmWasmClient]>
recoverMnemonic: (password: string, filename?: string) => Promise<string>
}
const useOptions = (options: Options): Network => {
const loadOrCreateWallet = async (options: Options, filename: string, password: string): Promise<DirectSecp256k1HdWallet> => {
let encrypted: string;
try {
encrypted = fs.readFileSync(filename, 'utf8');
} catch (err) {
// generate if no file exists
const wallet = await DirectSecp256k1HdWallet.generate(12, {hdPaths: [options.hdPath], prefix: options.bech32prefix});
const encrypted = await wallet.serialize(password);
fs.writeFileSync(filename, encrypted, 'utf8');
return wallet;
}
// otherwise, decrypt the file (we cannot put deserialize inside try or it will over-write on a bad password)
const wallet = await DirectSecp256k1HdWallet.deserialize(encrypted, password);
return wallet;
};
const connect = async (
wallet: DirectSecp256k1HdWallet,
options: Options
): Promise<SigningCosmWasmClient> => {
const clientOptions = {
prefix: options.bech32prefix
}
return await SigningCosmWasmClient.connectWithSigner(options.httpUrl, wallet, clientOptions)
};
const hitFaucet = async (
faucetUrl: string,
address: string,
denom: string
): Promise<void> => {
await axios.post(faucetUrl, { denom, address });
}
const setup = async (password: string, filename?: string): Promise<[string, SigningCosmWasmClient]> => {
const keyfile = filename || options.defaultKeyFile;
const wallet = await loadOrCreateWallet(pebblenetOptions, keyfile, password);
const client = await connect(wallet, pebblenetOptions);
const [account] = await wallet.getAccounts();
// ensure we have some tokens
if (options.faucetUrl) {
const tokens = await client.getBalance(account.address, options.feeToken)
if (tokens.amount === '0') {
console.log(`Getting ${options.feeToken} from faucet`);
await hitFaucet(options.faucetUrl, account.address, options.feeToken);
}
}
return [account.address, client];
}
const recoverMnemonic = async (password: string, filename?: string): Promise<string> => {
const keyfile = filename || options.defaultKeyFile;
const wallet = await loadOrCreateWallet(pebblenetOptions, keyfile, password);
return wallet.mnemonic;
}
return { setup, recoverMnemonic };
}
type TokenId = string
interface Balances {
readonly address: string
readonly amount: string // decimal as string
}
interface MintInfo {
readonly minter: string
readonly cap?: string // decimal as string
}
interface ContractInfo {
readonly name: string
readonly symbol: string
}
interface NftInfo {
readonly name: string,
readonly description: string,
readonly image: any
}
interface Access {
readonly owner: string,
readonly approvals: []
}
interface AllNftInfo {
readonly access: Access,
readonly info: NftInfo
}
interface Operators {
readonly operators: []
}
interface Count {
readonly count: number
}
interface InitMsg {
readonly name: string
readonly symbol: string
readonly minter: string
}
// Better to use this interface?
interface MintMsg {
readonly token_id: TokenId
readonly owner: string
readonly name: string
readonly description?: string
readonly image?: string
}
type Expiration = { readonly at_height: number } | { readonly at_time: number } | { readonly never: {} };
interface AllowanceResponse {
readonly allowance: string; // integer as string
readonly expires: Expiration;
}
interface AllowanceInfo {
readonly allowance: string; // integer as string
readonly spender: string; // bech32 address
readonly expires: Expiration;
}
interface AllAllowancesResponse {
readonly allowances: readonly AllowanceInfo[];
}
interface AllAccountsResponse {
// list of bech32 address that have a balance
readonly accounts: readonly string[];
}
interface TokensResponse {
readonly tokens: readonly string[];
}
interface CW721Instance {
readonly contractAddress: string
// queries
allowance: (owner: string, spender: string) => Promise<AllowanceResponse>
allAllowances: (owner: string, startAfter?: string, limit?: number) => Promise<AllAllowancesResponse>
allAccounts: (startAfter?: string, limit?: number) => Promise<readonly string[]>
minter: () => Promise<MintInfo>
contractInfo: () => Promise<ContractInfo>
nftInfo: (tokenId: TokenId) => Promise<NftInfo>
allNftInfo: (tokenId: TokenId) => Promise<AllNftInfo>
ownerOf: (tokenId: TokenId) => Promise<Access>
approvedForAll: (owner: string, include_expired?: boolean, start_after?: string, limit?: number) => Promise<Operators>
numTokens: () => Promise<Count>
tokens: (owner: string, startAfter?: string, limit?: number) => Promise<TokensResponse>
allTokens: (startAfter?: string, limit?: number) => Promise<TokensResponse>
// actions
mint: (senderAddress: string, tokenId: TokenId, owner: string, name: string, level: number, description?: string, image?: string) => Promise<string>
transferNft: (senderAddress: string, recipient: string, tokenId: TokenId) => Promise<string>
sendNft: (senderAddress: string, contract: string, token_id: TokenId, msg?: BinaryType) => Promise<string>
approve: (senderAddress: string, spender: string, tokenId: TokenId, expires?: Expiration) => Promise<string>
approveAll: (senderAddress: string, operator: string, expires?: Expiration) => Promise<string>
revoke: (senderAddress: string, spender: string, tokenId: TokenId) => Promise<string>
revokeAll: (senderAddress: string, operator: string) => Promise<string>
}
interface CW721Contract {
// upload a code blob and returns a codeId
upload: (senderAddress: string) => Promise<number>
// instantiates a cw721 contract
// codeId must come from a previous deploy
// label is the public name of the contract in listing
// if you set admin, you can run migrations on this contract (likely client.senderAddress)
instantiate: (senderAddress: string, codeId: number, initMsg: Record<string, unknown>, label: string, admin?: string) => Promise<CW721Instance>
use: (contractAddress: string) => CW721Instance
}
export const CW721 = (client: SigningCosmWasmClient, fees: Options['fees']): CW721Contract => {
const use = (contractAddress: string): CW721Instance => {
const allowance = async (owner: string, spender: string): Promise<AllowanceResponse> => {
return client.queryContractSmart(contractAddress, { allowance: { owner, spender } });
};
const allAllowances = async (owner: string, startAfter?: string, limit?: number): Promise<AllAllowancesResponse> => {
return client.queryContractSmart(contractAddress, { all_allowances: { owner, start_after: startAfter, limit } });
};
const allAccounts = async (startAfter?: string, limit?: number): Promise<readonly string[]> => {
const accounts: AllAccountsResponse = await client.queryContractSmart(contractAddress, { all_accounts: { start_after: startAfter, limit } });
return accounts.accounts;
};
const minter = async (): Promise<MintInfo> => {
return client.queryContractSmart(contractAddress, { minter: {} });
};
const contractInfo = async (): Promise<ContractInfo> => {
return client.queryContractSmart(contractAddress, { contract_info: {} });
};
const nftInfo = async (token_id: TokenId): Promise<NftInfo> => {
return client.queryContractSmart(contractAddress, { nft_info: { token_id } });
}
const allNftInfo = async (token_id: TokenId): Promise<AllNftInfo> => {
return client.queryContractSmart(contractAddress, { all_nft_info: { token_id } });
}
const ownerOf = async (token_id: TokenId): Promise<Access> => {
return await client.queryContractSmart(contractAddress, { owner_of: { token_id } });
}
const approvedForAll = async (owner: string, include_expired?: boolean, start_after?: string, limit?: number): Promise<Operators> => {
return await client.queryContractSmart(contractAddress, { approved_for_all: { owner, include_expired, start_after, limit } })
}
// total number of tokens issued
const numTokens = async (): Promise<Count> => {
return client.queryContractSmart(contractAddress, { num_tokens: {} });
}
// list all token_ids that belong to a given owner
const tokens = async (owner: string, start_after?: string, limit?: number): Promise<TokensResponse> => {
return client.queryContractSmart(contractAddress, { tokens: { owner, start_after, limit } });
}
const allTokens = async (start_after?: string, limit?: number): Promise<TokensResponse> => {
return client.queryContractSmart(contractAddress, { all_tokens: { start_after, limit } });
}
// actions
const mint = async (senderAddress: string, token_id: TokenId, owner: string, name: string, level: number, description?: string, image?: string): Promise<string> => {
const result = await client.execute(senderAddress, contractAddress, { mint: { token_id, owner, name, level, description, image } }, fees.exec);
return result.transactionHash;
}
// transfers ownership, returns transactionHash
const transferNft = async (senderAddress: string, recipient: string, token_id: TokenId): Promise<string> => {
const result = await client.execute(senderAddress, contractAddress, { transfer_nft: { recipient, token_id } }, fees.exec);
return result.transactionHash;
}
// sends an nft token to another contract (TODO: msg type any needs to be revisited once receiveNft is implemented)
const sendNft = async (senderAddress: string, contract: string, token_id: TokenId, msg?: any): Promise<string> => {
const result = await client.execute(senderAddress, contractAddress, { send_nft: { contract, token_id, msg } }, fees.exec)
return result.transactionHash;
}
const approve = async (senderAddress: string, spender: string, token_id: TokenId, expires?: Expiration): Promise<string> => {
const result = await client.execute(senderAddress, contractAddress, { approve: { spender, token_id, expires } }, fees.exec);
return result.transactionHash;
}
const approveAll = async (senderAddress: string, operator: string, expires?: Expiration): Promise<string> => {
const result = await client.execute(senderAddress, contractAddress, { approve_all: { operator, expires } }, fees.exec)
return result.transactionHash
}
const revoke = async (senderAddress: string, spender: string, token_id: TokenId): Promise<string> => {
const result = await client.execute(senderAddress, contractAddress, { revoke: { spender, token_id } }, fees.exec);
return result.transactionHash;
}
const revokeAll = async (senderAddress: string, operator: string): Promise<string> => {
const result = await client.execute(senderAddress, contractAddress, { revoke_all: { operator } }, fees.exec)
return result.transactionHash;
}
return {
contractAddress,
allowance,
allAllowances,
allAccounts,
minter,
contractInfo,
nftInfo,
allNftInfo,
ownerOf,
approvedForAll,
numTokens,
tokens,
allTokens,
mint,
transferNft,
sendNft,
approve,
approveAll,
revoke,
revokeAll
};
}
const downloadWasm = async (url: string): Promise<Uint8Array> => {
const r = await axios.get(url, { responseType: 'arraybuffer' })
if (r.status !== 200) {
throw new Error(`Download error: ${r.status}`)
}
return r.data
}
const upload = async (senderAddress: string): Promise<number> => {
const sourceUrl = "https://github.com/CosmWasm/cosmwasm-plus/releases/download/v0.9.0/cw721_base.wasm";
const wasm = await downloadWasm(sourceUrl);
const result = await client.upload(senderAddress, wasm, fees.upload);
return result.codeId;
}
const instantiate = async (senderAddress: string, codeId: number, initMsg: Record<string, unknown>, label: string, admin?: string): Promise<CW721Instance> => {
const result = await client.instantiate(senderAddress, codeId, initMsg, label, fees.init, { memo: `Init ${label}`, admin });
return use(result.contractAddress);
}
return { upload, instantiate, use };
}