helpers.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393
  1. import axios from "axios";
  2. import fs from "fs";
  3. import { SigningCosmWasmClient } from "@cosmjs/cosmwasm-stargate";
  4. import { GasPrice, calculateFee, StdFee } from "@cosmjs/stargate";
  5. import { DirectSecp256k1HdWallet, makeCosmoshubPath } from "@cosmjs/proto-signing";
  6. import { Slip10RawIndex } from "@cosmjs/crypto";
  7. import path from "path";
  8. /*
  9. * This is a set of helpers meant for use with @cosmjs/cli
  10. * With these you can easily use the cw721 contract without worrying about forming messages and parsing queries.
  11. *
  12. * Usage: npx @cosmjs/cli@^0.26 --init https://raw.githubusercontent.com/CosmWasm/cosmwasm-plus/master/contracts/cw721-base/helpers.ts
  13. *
  14. * Create a client:
  15. * const [addr, client] = await useOptions(pebblenetOptions).setup('password');
  16. *
  17. * Get the mnemonic:
  18. * await useOptions(pebblenetOptions).recoverMnemonic(password);
  19. *
  20. * Create contract:
  21. * const contract = CW721(client, pebblenetOptions.fees);
  22. *
  23. * Upload contract:
  24. * const codeId = await contract.upload(addr);
  25. *
  26. * Instantiate contract example:
  27. * const initMsg = {
  28. * name: "Potato Coin",
  29. * symbol: "TATER",
  30. * minter: addr
  31. * };
  32. * const instance = await contract.instantiate(addr, codeId, initMsg, 'Potato Coin!');
  33. * If you want to use this code inside an app, you will need several imports from https://github.com/CosmWasm/cosmjs
  34. */
  35. interface Options {
  36. readonly httpUrl: string
  37. readonly networkId: string
  38. readonly feeToken: string
  39. readonly bech32prefix: string
  40. readonly hdPath: readonly Slip10RawIndex[]
  41. readonly faucetUrl?: string
  42. readonly defaultKeyFile: string,
  43. readonly fees: {
  44. upload: StdFee,
  45. init: StdFee,
  46. exec: StdFee
  47. }
  48. }
  49. const pebblenetGasPrice = GasPrice.fromString("0.01upebble");
  50. const pebblenetOptions: Options = {
  51. httpUrl: 'https://rpc.pebblenet.cosmwasm.com',
  52. networkId: 'pebblenet-1',
  53. bech32prefix: 'wasm',
  54. feeToken: 'upebble',
  55. faucetUrl: 'https://faucet.pebblenet.cosmwasm.com/credit',
  56. hdPath: makeCosmoshubPath(0),
  57. defaultKeyFile: path.join(process.env.HOME, ".pebblenet.key"),
  58. fees: {
  59. upload: calculateFee(1500000, pebblenetGasPrice),
  60. init: calculateFee(500000, pebblenetGasPrice),
  61. exec: calculateFee(200000, pebblenetGasPrice),
  62. },
  63. }
  64. interface Network {
  65. setup: (password: string, filename?: string) => Promise<[string, SigningCosmWasmClient]>
  66. recoverMnemonic: (password: string, filename?: string) => Promise<string>
  67. }
  68. const useOptions = (options: Options): Network => {
  69. const loadOrCreateWallet = async (options: Options, filename: string, password: string): Promise<DirectSecp256k1HdWallet> => {
  70. let encrypted: string;
  71. try {
  72. encrypted = fs.readFileSync(filename, 'utf8');
  73. } catch (err) {
  74. // generate if no file exists
  75. const wallet = await DirectSecp256k1HdWallet.generate(12, {hdPaths: [options.hdPath], prefix: options.bech32prefix});
  76. const encrypted = await wallet.serialize(password);
  77. fs.writeFileSync(filename, encrypted, 'utf8');
  78. return wallet;
  79. }
  80. // otherwise, decrypt the file (we cannot put deserialize inside try or it will over-write on a bad password)
  81. const wallet = await DirectSecp256k1HdWallet.deserialize(encrypted, password);
  82. return wallet;
  83. };
  84. const connect = async (
  85. wallet: DirectSecp256k1HdWallet,
  86. options: Options
  87. ): Promise<SigningCosmWasmClient> => {
  88. const clientOptions = {
  89. prefix: options.bech32prefix
  90. }
  91. return await SigningCosmWasmClient.connectWithSigner(options.httpUrl, wallet, clientOptions)
  92. };
  93. const hitFaucet = async (
  94. faucetUrl: string,
  95. address: string,
  96. denom: string
  97. ): Promise<void> => {
  98. await axios.post(faucetUrl, { denom, address });
  99. }
  100. const setup = async (password: string, filename?: string): Promise<[string, SigningCosmWasmClient]> => {
  101. const keyfile = filename || options.defaultKeyFile;
  102. const wallet = await loadOrCreateWallet(pebblenetOptions, keyfile, password);
  103. const client = await connect(wallet, pebblenetOptions);
  104. const [account] = await wallet.getAccounts();
  105. // ensure we have some tokens
  106. if (options.faucetUrl) {
  107. const tokens = await client.getBalance(account.address, options.feeToken)
  108. if (tokens.amount === '0') {
  109. console.log(`Getting ${options.feeToken} from faucet`);
  110. await hitFaucet(options.faucetUrl, account.address, options.feeToken);
  111. }
  112. }
  113. return [account.address, client];
  114. }
  115. const recoverMnemonic = async (password: string, filename?: string): Promise<string> => {
  116. const keyfile = filename || options.defaultKeyFile;
  117. const wallet = await loadOrCreateWallet(pebblenetOptions, keyfile, password);
  118. return wallet.mnemonic;
  119. }
  120. return { setup, recoverMnemonic };
  121. }
  122. type TokenId = string
  123. interface Balances {
  124. readonly address: string
  125. readonly amount: string // decimal as string
  126. }
  127. interface MintInfo {
  128. readonly minter: string
  129. readonly cap?: string // decimal as string
  130. }
  131. interface ContractInfo {
  132. readonly name: string
  133. readonly symbol: string
  134. }
  135. interface NftInfo {
  136. readonly name: string,
  137. readonly description: string,
  138. readonly image: any
  139. }
  140. interface Access {
  141. readonly owner: string,
  142. readonly approvals: []
  143. }
  144. interface AllNftInfo {
  145. readonly access: Access,
  146. readonly info: NftInfo
  147. }
  148. interface Operators {
  149. readonly operators: []
  150. }
  151. interface Count {
  152. readonly count: number
  153. }
  154. interface InitMsg {
  155. readonly name: string
  156. readonly symbol: string
  157. readonly minter: string
  158. }
  159. // Better to use this interface?
  160. interface MintMsg {
  161. readonly token_id: TokenId
  162. readonly owner: string
  163. readonly name: string
  164. readonly description?: string
  165. readonly image?: string
  166. }
  167. type Expiration = { readonly at_height: number } | { readonly at_time: number } | { readonly never: {} };
  168. interface AllowanceResponse {
  169. readonly allowance: string; // integer as string
  170. readonly expires: Expiration;
  171. }
  172. interface AllowanceInfo {
  173. readonly allowance: string; // integer as string
  174. readonly spender: string; // bech32 address
  175. readonly expires: Expiration;
  176. }
  177. interface AllAllowancesResponse {
  178. readonly allowances: readonly AllowanceInfo[];
  179. }
  180. interface AllAccountsResponse {
  181. // list of bech32 address that have a balance
  182. readonly accounts: readonly string[];
  183. }
  184. interface TokensResponse {
  185. readonly tokens: readonly string[];
  186. }
  187. interface CW721Instance {
  188. readonly contractAddress: string
  189. // queries
  190. allowance: (owner: string, spender: string) => Promise<AllowanceResponse>
  191. allAllowances: (owner: string, startAfter?: string, limit?: number) => Promise<AllAllowancesResponse>
  192. allAccounts: (startAfter?: string, limit?: number) => Promise<readonly string[]>
  193. minter: () => Promise<MintInfo>
  194. contractInfo: () => Promise<ContractInfo>
  195. nftInfo: (tokenId: TokenId) => Promise<NftInfo>
  196. allNftInfo: (tokenId: TokenId) => Promise<AllNftInfo>
  197. ownerOf: (tokenId: TokenId) => Promise<Access>
  198. approvedForAll: (owner: string, include_expired?: boolean, start_after?: string, limit?: number) => Promise<Operators>
  199. numTokens: () => Promise<Count>
  200. tokens: (owner: string, startAfter?: string, limit?: number) => Promise<TokensResponse>
  201. allTokens: (startAfter?: string, limit?: number) => Promise<TokensResponse>
  202. // actions
  203. mint: (senderAddress: string, tokenId: TokenId, owner: string, name: string, level: number, description?: string, image?: string) => Promise<string>
  204. transferNft: (senderAddress: string, recipient: string, tokenId: TokenId) => Promise<string>
  205. sendNft: (senderAddress: string, contract: string, token_id: TokenId, msg?: BinaryType) => Promise<string>
  206. approve: (senderAddress: string, spender: string, tokenId: TokenId, expires?: Expiration) => Promise<string>
  207. approveAll: (senderAddress: string, operator: string, expires?: Expiration) => Promise<string>
  208. revoke: (senderAddress: string, spender: string, tokenId: TokenId) => Promise<string>
  209. revokeAll: (senderAddress: string, operator: string) => Promise<string>
  210. }
  211. interface CW721Contract {
  212. // upload a code blob and returns a codeId
  213. upload: (senderAddress: string) => Promise<number>
  214. // instantiates a cw721 contract
  215. // codeId must come from a previous deploy
  216. // label is the public name of the contract in listing
  217. // if you set admin, you can run migrations on this contract (likely client.senderAddress)
  218. instantiate: (senderAddress: string, codeId: number, initMsg: Record<string, unknown>, label: string, admin?: string) => Promise<CW721Instance>
  219. use: (contractAddress: string) => CW721Instance
  220. }
  221. export const CW721 = (client: SigningCosmWasmClient, fees: Options['fees']): CW721Contract => {
  222. const use = (contractAddress: string): CW721Instance => {
  223. const allowance = async (owner: string, spender: string): Promise<AllowanceResponse> => {
  224. return client.queryContractSmart(contractAddress, { allowance: { owner, spender } });
  225. };
  226. const allAllowances = async (owner: string, startAfter?: string, limit?: number): Promise<AllAllowancesResponse> => {
  227. return client.queryContractSmart(contractAddress, { all_allowances: { owner, start_after: startAfter, limit } });
  228. };
  229. const allAccounts = async (startAfter?: string, limit?: number): Promise<readonly string[]> => {
  230. const accounts: AllAccountsResponse = await client.queryContractSmart(contractAddress, { all_accounts: { start_after: startAfter, limit } });
  231. return accounts.accounts;
  232. };
  233. const minter = async (): Promise<MintInfo> => {
  234. return client.queryContractSmart(contractAddress, { minter: {} });
  235. };
  236. const contractInfo = async (): Promise<ContractInfo> => {
  237. return client.queryContractSmart(contractAddress, { contract_info: {} });
  238. };
  239. const nftInfo = async (token_id: TokenId): Promise<NftInfo> => {
  240. return client.queryContractSmart(contractAddress, { nft_info: { token_id } });
  241. }
  242. const allNftInfo = async (token_id: TokenId): Promise<AllNftInfo> => {
  243. return client.queryContractSmart(contractAddress, { all_nft_info: { token_id } });
  244. }
  245. const ownerOf = async (token_id: TokenId): Promise<Access> => {
  246. return await client.queryContractSmart(contractAddress, { owner_of: { token_id } });
  247. }
  248. const approvedForAll = async (owner: string, include_expired?: boolean, start_after?: string, limit?: number): Promise<Operators> => {
  249. return await client.queryContractSmart(contractAddress, { approved_for_all: { owner, include_expired, start_after, limit } })
  250. }
  251. // total number of tokens issued
  252. const numTokens = async (): Promise<Count> => {
  253. return client.queryContractSmart(contractAddress, { num_tokens: {} });
  254. }
  255. // list all token_ids that belong to a given owner
  256. const tokens = async (owner: string, start_after?: string, limit?: number): Promise<TokensResponse> => {
  257. return client.queryContractSmart(contractAddress, { tokens: { owner, start_after, limit } });
  258. }
  259. const allTokens = async (start_after?: string, limit?: number): Promise<TokensResponse> => {
  260. return client.queryContractSmart(contractAddress, { all_tokens: { start_after, limit } });
  261. }
  262. // actions
  263. const mint = async (senderAddress: string, token_id: TokenId, owner: string, name: string, level: number, description?: string, image?: string): Promise<string> => {
  264. const result = await client.execute(senderAddress, contractAddress, { mint: { token_id, owner, name, level, description, image } }, fees.exec);
  265. return result.transactionHash;
  266. }
  267. // transfers ownership, returns transactionHash
  268. const transferNft = async (senderAddress: string, recipient: string, token_id: TokenId): Promise<string> => {
  269. const result = await client.execute(senderAddress, contractAddress, { transfer_nft: { recipient, token_id } }, fees.exec);
  270. return result.transactionHash;
  271. }
  272. // sends an nft token to another contract (TODO: msg type any needs to be revisited once receiveNft is implemented)
  273. const sendNft = async (senderAddress: string, contract: string, token_id: TokenId, msg?: any): Promise<string> => {
  274. const result = await client.execute(senderAddress, contractAddress, { send_nft: { contract, token_id, msg } }, fees.exec)
  275. return result.transactionHash;
  276. }
  277. const approve = async (senderAddress: string, spender: string, token_id: TokenId, expires?: Expiration): Promise<string> => {
  278. const result = await client.execute(senderAddress, contractAddress, { approve: { spender, token_id, expires } }, fees.exec);
  279. return result.transactionHash;
  280. }
  281. const approveAll = async (senderAddress: string, operator: string, expires?: Expiration): Promise<string> => {
  282. const result = await client.execute(senderAddress, contractAddress, { approve_all: { operator, expires } }, fees.exec)
  283. return result.transactionHash
  284. }
  285. const revoke = async (senderAddress: string, spender: string, token_id: TokenId): Promise<string> => {
  286. const result = await client.execute(senderAddress, contractAddress, { revoke: { spender, token_id } }, fees.exec);
  287. return result.transactionHash;
  288. }
  289. const revokeAll = async (senderAddress: string, operator: string): Promise<string> => {
  290. const result = await client.execute(senderAddress, contractAddress, { revoke_all: { operator } }, fees.exec)
  291. return result.transactionHash;
  292. }
  293. return {
  294. contractAddress,
  295. allowance,
  296. allAllowances,
  297. allAccounts,
  298. minter,
  299. contractInfo,
  300. nftInfo,
  301. allNftInfo,
  302. ownerOf,
  303. approvedForAll,
  304. numTokens,
  305. tokens,
  306. allTokens,
  307. mint,
  308. transferNft,
  309. sendNft,
  310. approve,
  311. approveAll,
  312. revoke,
  313. revokeAll
  314. };
  315. }
  316. const downloadWasm = async (url: string): Promise<Uint8Array> => {
  317. const r = await axios.get(url, { responseType: 'arraybuffer' })
  318. if (r.status !== 200) {
  319. throw new Error(`Download error: ${r.status}`)
  320. }
  321. return r.data
  322. }
  323. const upload = async (senderAddress: string): Promise<number> => {
  324. const sourceUrl = "https://github.com/CosmWasm/cosmwasm-plus/releases/download/v0.9.0/cw721_base.wasm";
  325. const wasm = await downloadWasm(sourceUrl);
  326. const result = await client.upload(senderAddress, wasm, fees.upload);
  327. return result.codeId;
  328. }
  329. const instantiate = async (senderAddress: string, codeId: number, initMsg: Record<string, unknown>, label: string, admin?: string): Promise<CW721Instance> => {
  330. const result = await client.instantiate(senderAddress, codeId, initMsg, label, fees.init, { memo: `Init ${label}`, admin });
  331. return use(result.contractAddress);
  332. }
  333. return { upload, instantiate, use };
  334. }