import { StdSignature, StdSignDoc } from "@cosmjs/amino"; import { fromBase64 } from "@cosmjs/encoding"; import { OfflineSigner } from "@cosmjs/proto-signing"; import { AminoTypes, DeliverTxResponse } from "@cosmjs/stargate"; import { createDefaultAminoConverters, SigningStargateClient, } from "@cosmjs/stargate/build/signingstargateclient"; import { MsgSend } from "cosmjs-types/cosmos/bank/v1beta1/tx"; import { MsgWithdrawDelegatorReward } from "cosmjs-types/cosmos/distribution/v1beta1/tx"; import { MsgBeginRedelegate, MsgDelegate, MsgUndelegate, } from "cosmjs-types/cosmos/staking/v1beta1/tx"; import { TxRaw } from "cosmjs-types/cosmos/tx/v1beta1/tx"; import { TxRejected } from "./constants/errorCodes"; import { FAVORITE_ASSET_URI } from "./constants/favoriteAssetUri"; import { MsgCreateEscrow, MsgRefundEscrow, MsgTransferToEscrow, MsgUpdateEscrow, } from "./proto/iov/escrow/v1beta1/tx"; import { MsgAddAccountCertificate, MsgDeleteAccount, MsgDeleteAccountCertificate, MsgDeleteDomain, MsgRegisterAccount, MsgRegisterDomain, MsgRenewAccount, MsgRenewDomain, MsgReplaceAccountMetadata, MsgReplaceAccountResources, MsgTransferAccount, MsgTransferDomain, } from "./proto/iov/starname/v1beta1/tx"; import { Resource } from "./proto/iov/starname/v1beta1/types"; import { StarnameClient } from "./starnameClient"; import { Task } from "./starnameClient/task"; import { StarnameRegistry, TxType } from "./starnameRegistry"; import { customStarnameAminoTypes } from "./types/aminoTypes"; import { Amount } from "./types/amount"; import { ResponsePage } from "./types/apiPage"; import { AssetResource } from "./types/assetResource"; import { FeeEstimator } from "./types/feeEstimator"; import { MsgsAndMemo } from "./types/msgsAndMemo"; import { Starname } from "./types/starname"; import { TokenLike } from "./types/tokenLike"; import { Transaction } from "./types/transaction"; import { Tx } from "./types/tx"; import { constructTransferrableObject } from "./utils/constructTransferrableObject"; import { createResourcesFromAddressGroup } from "./utils/createResourcesFromAddressGroup"; import { estimateFee, GasConfig } from "./utils/estimateFee"; import { Buffer } from "buffer/"; import { defaultGasConfig } from "./utils/defaultGasConfig"; import { ChainMap, Signer, SignerType } from "@iov/signer-types"; export interface WalletOptions { readonly feeEstimator?: FeeEstimator; readonly gasConfig?: GasConfig; } export class Wallet { protected readonly starnameClient: StarnameClient; private readonly signer: Signer; private readonly feeEstimator: FeeEstimator; private readonly gasConfig: GasConfig; constructor(signer: Signer, client: StarnameClient, options?: WalletOptions) { this.starnameClient = client; this.signer = signer; this.feeEstimator = options?.feeEstimator ?? estimateFee; this.gasConfig = options?.gasConfig ?? { gasMap: defaultGasConfig.gasMap, gasPrice: { ...defaultGasConfig.gasPrice, denom: client.getMainToken().subunitName, }, }; } protected async signAndBroadcast( msgsAndMemo: MsgsAndMemo, ): Promise { const { starnameClient } = this; try { // signing const txRaw = await this.signMsgsAndMemo(msgsAndMemo); // transpile const bytes = Uint8Array.from(TxRaw.encode(txRaw).finish()); // broadcast return starnameClient.broadcastTx(bytes); } catch (exception: any) { if (exception.message !== "Request rejected") { throw exception; } else { return { transactionHash: "", txIndex: 0, height: 0, code: TxRejected, rawLog: "", events: [], gasUsed: 0, gasWanted: 0, }; } } } private async signWithSigner( signer: OfflineSigner, msgsAndMemo: MsgsAndMemo, ): Promise { const { messages, memo } = msgsAndMemo; const { starnameClient } = this; const address = await this.getAddress(); const fee = this.feeEstimator(messages, this.gasConfig); const registry = new StarnameRegistry(); const client = await SigningStargateClient.offline(signer, { registry, aminoTypes: new AminoTypes({ ...createDefaultAminoConverters(), ...customStarnameAminoTypes, }), }); const account = await starnameClient.getAccount(address); if (!account) { throw new Error(`could not find account for ${address}`); } return client.sign(address, messages, fee, memo, { accountNumber: account.accountNumber, sequence: account.sequence, chainId: starnameClient.getChainId(), }); } public async signMsgsAndMemo(msgsAndMemo: MsgsAndMemo): Promise { const { signer } = this; return this.signWithSigner(signer.getOfflineSigner(), msgsAndMemo); } public getPublicKey(): Promise { const { signer } = this; return signer.getPublicKey(); } public async getAddress(): Promise { const { signer } = this; return signer.getAddress(); } public getSigner(): Signer { return this.signer; } public disconnect(): void { return this.signer.disconnect(); } public getBalances(): Task> { let task: Task> | null = null; return { run: async (): Promise> => { const { starnameClient } = this; const signer: Signer = this.getSigner(); task = starnameClient.getBalance(await signer.getAddress()); return task.run(); }, abort: (): void => { if (task !== null) { task.abort(); } }, }; } public getTransactions(): Task>> { let task: Task>> | null = null; return { run: async (): Promise>> => { const { starnameClient } = this; const signer: Signer = this.getSigner(); task = starnameClient.getTransactions(await signer.getAddress()); return task.run(); }, abort: (): void => { if (task !== null) { task.abort(); } }, }; } protected static buildPreferredAssetItem( preferredAsset: string, ): ReadonlyArray { if (preferredAsset === "") return []; return [ { uri: FAVORITE_ASSET_URI, resource: preferredAsset, }, ]; } public async replaceDomainResources( domain: string, targets: ReadonlyArray, profile: ReadonlyArray, preferredAsset: string, memo = "", ): Promise { const address = await this.getAddress(); const message: Tx = { typeUrl: TxType.Starname.ReplaceAccountResources, value: { domain: domain, name: "", newResources: [ ...targets.map( ({ asset, address }: AssetResource): Resource => ({ uri: asset["starname-uri"], resource: address, }), ), ...Wallet.buildPreferredAssetItem(preferredAsset), ...profile, ], owner: address, payer: "", }, }; return this.signAndBroadcast({ messages: [message], memo, }); } protected sanitizeResources( resources: ReadonlyArray, ): Array { return resources .map((item: Resource): Resource => { const value: string = item.resource; return { uri: item.uri, resource: value.trim(), }; }) .filter(({ resource: value }: Resource): boolean => value !== ""); } public async replaceAccountResources( name: string, domain: string, targets: ReadonlyArray, profile: ReadonlyArray, preferredAsset: string, memo = "", ): Promise { const address = await this.getAddress(); const message: Tx = { typeUrl: TxType.Starname.ReplaceAccountResources, value: { domain: domain, name: name, newResources: this.sanitizeResources([ ...targets.map( ({ asset, address }: AssetResource): Resource => ({ uri: asset["starname-uri"], resource: address, }), ), ...Wallet.buildPreferredAssetItem(preferredAsset), ...profile, ]), owner: address, payer: "", }, }; return this.signAndBroadcast({ messages: [message], memo, }); } public async addAccountCertificate( name: string, domain: string, certificate: string, memo = "", ): Promise { const owner = await this.getAddress(); const addAccountCertificateMsg: Tx = { typeUrl: TxType.Starname.AddAccountCertificate, value: { domain, name, owner, newCertificate: fromBase64(Buffer.from(certificate).toString("base64")), payer: "", }, }; return this.signAndBroadcast({ messages: [addAccountCertificateMsg], memo, }); } /** * * @param name * @param domain * @param b64Certificate - A base64 encoded JSON certificate * @returns DeliverTxResponse */ public async deleteAccountCertificate( name: string, domain: string, b64Certificate: string, memo = "", ): Promise { const owner = await this.getAddress(); const deleteAccountCertificateMsg: Tx = { typeUrl: TxType.Starname.DeleteAccountCertificate, value: { name, domain, deleteCertificate: fromBase64(b64Certificate), owner, payer: "", }, }; return this.signAndBroadcast({ messages: [deleteAccountCertificateMsg], memo, }); } public async createEscrow( amount: Amount, item: Starname, deadline: Date, memo = "", ): Promise { if (this.getSignerType() === SignerType.Ledger) throw new Error("ledger unsupported"); const address = await this.getAddress(); const createEscrowMsg: Tx = { typeUrl: TxType.Escrow.CreateEscrow, value: { seller: address, feePayer: "", price: [...amount.toCoins()], deadline: Math.floor(deadline.getTime() / 1000), object: constructTransferrableObject(item), }, }; return this.signAndBroadcast({ messages: [createEscrowMsg], memo, }); } public async updateEscrow( id: string, newAmount: Amount, newDeadline: Date, newSeller: string, memo = "", ): Promise { if (this.getSignerType() === SignerType.Ledger) throw new Error("ledger unsupported"); const address = await this.getAddress(); const updateEscrowMsg: Tx = { typeUrl: TxType.Escrow.UpdateEscrow, value: { id, feePayer: "", price: [...newAmount.toCoins()], deadline: Math.floor(newDeadline.getTime() / 1000), seller: newSeller, updater: address, }, }; return this.signAndBroadcast({ messages: [updateEscrowMsg], memo, }); } public async transferToEscrow( id: string, amount: Amount, memo = "", ): Promise { if (this.getSignerType() === SignerType.Ledger) throw new Error("ledger unsupported"); const address = await this.getAddress(); const transferToEscrowMsg: Tx = { typeUrl: TxType.Escrow.TransferToEscrow, value: { id, feePayer: "", amount: [...amount.toCoins()], sender: address, }, }; return this.signAndBroadcast({ messages: [transferToEscrowMsg], memo, }); } public async deleteEscrow(id: string, memo = ""): Promise { if (this.getSignerType() === SignerType.Ledger) throw new Error("ledger unsupported"); const address = await this.getAddress(); const refundEscrowMsg: Tx = { typeUrl: TxType.Escrow.RefundEscrow, value: { id, sender: address, feePayer: "", }, }; return this.signAndBroadcast({ messages: [refundEscrowMsg], memo, }); } public async registerDomain( domain: string, type: "closed" | "open" = "closed", expired = false, memo = "", ): Promise { const { starnameClient } = this; const address: string = await this.getAddress(); const message: Tx = { typeUrl: TxType.Starname.RegisterDomain, value: { name: domain, admin: address, domainType: type, payer: "", broker: starnameClient.getBroker(), }, }; return this.signAndBroadcast({ messages: [ ...(expired ? [ { typeUrl: TxType.Starname.DeleteDomain, value: { domain: domain, owner: address, payer: "", }, }, ] : []), message, ], memo, }); } public async getOtherChainResources( chains: ChainMap, ): Promise> { const { signer } = this; switch (this.getSignerType()) { case SignerType.Keplr: case SignerType.Google: case SignerType.SeedPhrase: { // Now request signer to provide addressGroup ( for chains ) const addressGroup = await signer.getAddressGroup(chains); return createResourcesFromAddressGroup(addressGroup, chains); } default: return []; } } public async registerAccount( name: string, domain: string, chains?: ChainMap, expired = false, memo = "", ): Promise { const address: string = await this.getAddress(); const resources: Array = []; // Set default resource first resources.push({ uri: this.starnameClient.getDefaultAssetURI(), resource: address, }); // if require generation of other chain addresses as well if (chains) { try { const otherChainResources = await this.getOtherChainResources(chains); resources.push(...otherChainResources); } catch (error) { console.warn("Failure getting other chain resources"); } } const message: Tx = { typeUrl: TxType.Starname.RegisterAccount, value: { domain: domain, name: name, owner: address, registerer: address, resources: resources, broker: this.starnameClient.getBroker(), payer: "", }, }; return this.signAndBroadcast({ messages: [ ...(expired ? [ { typeUrl: TxType.Starname.DeleteAccount, value: { domain, name, owner: address, payer: "", }, }, ] : []), message, ], memo, }); } public async transferDomain( domain: string, recipient: string, transferFlag: 0 | 1 | 2 = 0, memo = "", ): Promise { const message: Tx = { typeUrl: TxType.Starname.TransferDomain, value: { domain: domain, owner: await this.getAddress(), newAdmin: recipient, transferFlag, payer: "", }, }; return this.signAndBroadcast({ messages: [message], memo, }); } public async transferAccount( name: string, domain: string, recipient: string, reset = true, memo = "", ): Promise { const address = await this.getAddress(); const message: Tx = { typeUrl: TxType.Starname.TransferAccount, value: { name: name, domain: domain, owner: address, newOwner: recipient, reset, payer: "", }, }; return this.signAndBroadcast({ messages: [message], memo, }); } public async deleteDomain( domain: string, memo = "", ): Promise { const address: string = await this.getAddress(); const message: Tx = { typeUrl: TxType.Starname.DeleteDomain, value: { domain: domain, owner: address, payer: "", }, }; return this.signAndBroadcast({ messages: [message], memo, }); } public async deleteAccount( name: string, domain: string, memo = "", ): Promise { const address: string = await this.getAddress(); const message: Tx = { typeUrl: TxType.Starname.DeleteAccount, value: { name: name, domain: domain, owner: address, payer: "", }, }; return this.signAndBroadcast({ messages: [message], memo, }); } public async renewDomain( domain: string, memo = "", ): Promise { const message: Tx = { typeUrl: TxType.Starname.RenewDomain, value: { domain: domain, signer: await this.getAddress(), payer: "", }, }; return this.signAndBroadcast({ messages: [message], memo, }); } public async renewAccount( name: string, domain: string, memo = "", ): Promise { const address = await this.getAddress(); const message: Tx = { typeUrl: TxType.Starname.RenewAccount, value: { domain: domain, name: name, signer: address, payer: "", }, }; return this.signAndBroadcast({ messages: [message], memo, }); } public async delegateAmount( validatorAddress: string, amount: Amount, memo = "", ): Promise { const address: string = await this.getAddress(); const message: Tx = { typeUrl: TxType.Staking.Delegate, value: { delegatorAddress: address, validatorAddress: validatorAddress, amount: amount.toCoins()[0], }, }; return this.signAndBroadcast({ messages: [message], memo, }); } public async unDelegateAmount( validatorAddress: string, amount: Amount, memo = "", ): Promise { const address: string = await this.getAddress(); const message: Tx = { typeUrl: TxType.Staking.Undelegate, value: { delegatorAddress: address, validatorAddress: validatorAddress, amount: amount.toCoins()[0], }, }; return this.signAndBroadcast({ messages: [message], memo, }); } public async redelegateAmount( validatorSource: string, validatorDestination: string, amount: Amount, memo = "", ): Promise { const address: string = await this.getAddress(); const message: Tx = { typeUrl: TxType.Staking.BeginRedelegate, value: { delegatorAddress: address, validatorSrcAddress: validatorSource, validatorDstAddress: validatorDestination, amount: amount.toCoins()[0], }, }; return this.signAndBroadcast({ messages: [message], memo, }); } public async claimReward( validatorAddresses: ReadonlyArray, memo = "", ): Promise { const address: string = await this.getAddress(); const composeMessage = ( validatorAddr: string, ): Tx => { return { typeUrl: TxType.Distribution.WithdrawDelegatorReward, value: { delegatorAddress: address, validatorAddress: validatorAddr, }, }; }; return this.signAndBroadcast({ messages: validatorAddresses.map(composeMessage), memo, }); } public async sendPayment( token: TokenLike, recipient: string, amount: number, memo = "", ): Promise { const address: string = await this.getAddress(); const uiov: number = amount * token.subunitsPerUnit; const message: Tx = { typeUrl: TxType.Bank.Send, value: { fromAddress: address, toAddress: recipient, amount: [ { amount: uiov.toFixed(0), denom: token.subunitName, }, ], }, }; return this.signAndBroadcast({ messages: [message], memo, }); } public async setMetadataURI( name: string, domain: string, uri: string | null, memo = "", ): Promise { const address = await this.getAddress(); const message: Tx = { typeUrl: TxType.Starname.ReplaceAccountMetadata, value: { domain: domain, name: name, newMetadataUri: uri !== null ? uri : "", owner: address, payer: "", }, }; return this.signAndBroadcast({ messages: [message], memo, }); } public getSignerType(): SignerType { const { signer } = this; return signer.type; } public async signAlephMessage(signDoc: StdSignDoc): Promise { const { signer } = this; const response = await signer.signAlephMessage( await this.getAddress(), signDoc, ); return response.signature; } }