Merge pull request #97 from confio/read-erc20
Add read support for ERC20 executions
This commit is contained in:
commit
1df7de4781
@ -81,7 +81,7 @@ export class CosmWasmCodec implements TxCodec {
|
||||
throw new Error("Nonce is required");
|
||||
}
|
||||
const parsed = unmarshalTx(bytes);
|
||||
return parseSignedTx(parsed, chainId, nonce, this.bankTokens);
|
||||
return parseSignedTx(parsed, chainId, nonce, this.bankTokens, this.erc20Tokens);
|
||||
}
|
||||
|
||||
public identityToAddress(identity: Identity): Address {
|
||||
|
||||
@ -49,6 +49,7 @@ const faucet = {
|
||||
|
||||
describe("CosmWasmConnection", () => {
|
||||
const cosm = "COSM" as TokenTicker;
|
||||
const bash = "BASH" as TokenTicker;
|
||||
const httpUrl = "http://localhost:1317";
|
||||
const defaultChainId = "cosmos:testing" as ChainId;
|
||||
const defaultEmptyAddress = "cosmos1h806c7khnvmjlywdrkdgk2vrayy2mmvf9rxk2r" as Address;
|
||||
@ -267,7 +268,7 @@ describe("CosmWasmConnection", () => {
|
||||
});
|
||||
|
||||
describe("getTx", () => {
|
||||
it("can get a recently posted transaction", async () => {
|
||||
it("can get a recently posted bank send transaction", async () => {
|
||||
pendingWithoutWasmd();
|
||||
const connection = await CosmWasmConnection.establish(httpUrl, defaultPrefix, defaultConfig);
|
||||
const profile = new UserProfile();
|
||||
@ -318,6 +319,56 @@ describe("CosmWasmConnection", () => {
|
||||
connection.disconnect();
|
||||
});
|
||||
|
||||
it("can get a recently posted ERC20 send transaction", async () => {
|
||||
pendingWithoutWasmd();
|
||||
const connection = await CosmWasmConnection.establish(httpUrl, defaultPrefix, defaultConfig);
|
||||
const profile = new UserProfile();
|
||||
const wallet = profile.addWallet(Secp256k1HdWallet.fromMnemonic(faucet.mnemonic));
|
||||
const senderIdentity = await profile.createIdentity(wallet.id, defaultChainId, faucet.path);
|
||||
const senderAddress = connection.codec.identityToAddress(senderIdentity);
|
||||
|
||||
const unsigned = await connection.withDefaultFee<SendTransaction>({
|
||||
kind: "bcp/send",
|
||||
chainId: defaultChainId,
|
||||
sender: senderAddress,
|
||||
recipient: defaultRecipient,
|
||||
memo: "An ERC20 payment",
|
||||
amount: {
|
||||
quantity: "345",
|
||||
fractionalDigits: 0,
|
||||
tokenTicker: bash,
|
||||
},
|
||||
});
|
||||
const nonce = await connection.getNonce({ address: senderAddress });
|
||||
const signed = await profile.signTransaction(senderIdentity, unsigned, connection.codec, nonce);
|
||||
const postableBytes = connection.codec.bytesToPost(signed);
|
||||
const response = await connection.postTx(postableBytes);
|
||||
const { transactionId } = response;
|
||||
await response.blockInfo.waitFor(info => isBlockInfoSucceeded(info));
|
||||
|
||||
const getResponse = await connection.getTx(transactionId);
|
||||
expect(getResponse.transactionId).toEqual(transactionId);
|
||||
assert(isConfirmedTransaction(getResponse), "Expected transaction to succeed");
|
||||
assert(getResponse.log, "Log must be available");
|
||||
const [firstLog] = JSON.parse(getResponse.log);
|
||||
expect(firstLog.events.length).toEqual(1);
|
||||
|
||||
const { transaction, signatures } = getResponse;
|
||||
assert(isSendTransaction(transaction), "Expected send transaction");
|
||||
expect(transaction).toEqual(unsigned);
|
||||
expect(signatures.length).toEqual(1);
|
||||
expect(signatures[0]).toEqual({
|
||||
nonce: signed.signatures[0].nonce,
|
||||
pubkey: {
|
||||
algo: signed.signatures[0].pubkey.algo,
|
||||
data: Secp256k1.compressPubkey(signed.signatures[0].pubkey.data),
|
||||
},
|
||||
signature: Secp256k1.trimRecoveryByte(signed.signatures[0].signature),
|
||||
});
|
||||
|
||||
connection.disconnect();
|
||||
});
|
||||
|
||||
it("can get an old transaction", async () => {
|
||||
pendingWithoutWasmd();
|
||||
const connection = await CosmWasmConnection.establish(httpUrl, defaultPrefix, defaultConfig);
|
||||
|
||||
@ -348,7 +348,13 @@ export class CosmWasmConnection implements BlockchainConnection {
|
||||
private parseAndPopulateTxResponseUnsigned(
|
||||
response: TxsResponse,
|
||||
): ConfirmedTransaction<UnsignedTransaction> | FailedTransaction {
|
||||
return parseTxsResponseUnsigned(this.chainId, parseInt(response.height, 10), response, this.bankTokens);
|
||||
return parseTxsResponseUnsigned(
|
||||
this.chainId,
|
||||
parseInt(response.height, 10),
|
||||
response,
|
||||
this.bankTokens,
|
||||
this.erc20Tokens,
|
||||
);
|
||||
}
|
||||
|
||||
private async parseAndPopulateTxResponseSigned(
|
||||
@ -387,6 +393,7 @@ export class CosmWasmConnection implements BlockchainConnection {
|
||||
nonce,
|
||||
response,
|
||||
this.bankTokens,
|
||||
this.erc20Tokens,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
/* eslint-disable @typescript-eslint/camelcase */
|
||||
import { types } from "@cosmwasm/sdk";
|
||||
import { Address, Algorithm, SendTransaction, TokenTicker } from "@iov/bcp";
|
||||
import { Address, Algorithm, isSendTransaction, SendTransaction, TokenTicker } from "@iov/bcp";
|
||||
import { Encoding } from "@iov/encoding";
|
||||
import { assert } from "@iov/utils";
|
||||
|
||||
import {
|
||||
decodeAmount,
|
||||
@ -17,7 +18,7 @@ import {
|
||||
} from "./decode";
|
||||
import * as testdata from "./testdata.spec";
|
||||
import cosmoshub from "./testdata/cosmoshub.json";
|
||||
import { BankTokens } from "./types";
|
||||
import { BankTokens, Erc20Token } from "./types";
|
||||
|
||||
const { fromBase64, fromHex } = Encoding;
|
||||
|
||||
@ -63,6 +64,23 @@ describe("decode", () => {
|
||||
denom: "uatom",
|
||||
},
|
||||
];
|
||||
const defaultErc20Tokens: Erc20Token[] = [
|
||||
{
|
||||
contractAddress: "cosmos18vd8fpwxzck93qlwghaj6arh4p7c5n89uzcee5",
|
||||
fractionalDigits: 5,
|
||||
ticker: "ASH",
|
||||
},
|
||||
{
|
||||
contractAddress: "cosmos1hqrdl6wstt8qzshwc6mrumpjk9338k0lr4dqxd",
|
||||
fractionalDigits: 0,
|
||||
ticker: "BASH",
|
||||
},
|
||||
{
|
||||
contractAddress: "cosmos18r5szma8hm93pvx6lwpjwyxruw27e0k5uw835c",
|
||||
fractionalDigits: 18,
|
||||
ticker: "CASH",
|
||||
},
|
||||
];
|
||||
|
||||
describe("decodePubkey", () => {
|
||||
it("works for secp256k1", () => {
|
||||
@ -126,7 +144,7 @@ describe("decode", () => {
|
||||
});
|
||||
|
||||
describe("parseMsg", () => {
|
||||
it("works", () => {
|
||||
it("works for bank send transaction", () => {
|
||||
const msg: types.Msg = {
|
||||
type: "cosmos-sdk/MsgSend",
|
||||
value: {
|
||||
@ -140,7 +158,40 @@ describe("decode", () => {
|
||||
],
|
||||
},
|
||||
};
|
||||
expect(parseMsg(msg, defaultMemo, testdata.chainId, defaultTokens)).toEqual(defaultSendTransaction);
|
||||
expect(parseMsg(msg, defaultMemo, testdata.chainId, defaultTokens, defaultErc20Tokens)).toEqual(
|
||||
defaultSendTransaction,
|
||||
);
|
||||
});
|
||||
|
||||
it("works for ERC20 send transaction", () => {
|
||||
const msg: types.MsgExecuteContract = {
|
||||
type: "wasm/execute",
|
||||
value: {
|
||||
sender: "cosmos1h806c7khnvmjlywdrkdgk2vrayy2mmvf9rxk2r",
|
||||
contract: defaultErc20Tokens[0].contractAddress,
|
||||
msg: {
|
||||
transfer: {
|
||||
amount: "887878484",
|
||||
recipient: "cosmos1z7g5w84ynmjyg0kqpahdjqpj7yq34v3suckp0e",
|
||||
},
|
||||
},
|
||||
sent_funds: [],
|
||||
},
|
||||
};
|
||||
const transaction = parseMsg(msg, defaultMemo, testdata.chainId, defaultTokens, defaultErc20Tokens);
|
||||
assert(isSendTransaction(transaction));
|
||||
expect(transaction).toEqual({
|
||||
kind: "bcp/send",
|
||||
chainId: testdata.chainId,
|
||||
sender: "cosmos1h806c7khnvmjlywdrkdgk2vrayy2mmvf9rxk2r" as Address,
|
||||
recipient: "cosmos1z7g5w84ynmjyg0kqpahdjqpj7yq34v3suckp0e" as Address,
|
||||
amount: {
|
||||
quantity: "887878484",
|
||||
tokenTicker: "ASH" as TokenTicker,
|
||||
fractionalDigits: 5,
|
||||
},
|
||||
memo: defaultMemo,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -160,18 +211,70 @@ describe("decode", () => {
|
||||
});
|
||||
|
||||
describe("parseUnsignedTx", () => {
|
||||
it("works", () => {
|
||||
expect(parseUnsignedTx(cosmoshub.tx.value, testdata.chainId, defaultTokens)).toEqual(
|
||||
testdata.sendTxJson,
|
||||
);
|
||||
it("works for bank send transaction", () => {
|
||||
expect(
|
||||
parseUnsignedTx(cosmoshub.tx.value, testdata.chainId, defaultTokens, defaultErc20Tokens),
|
||||
).toEqual(testdata.sendTxJson);
|
||||
});
|
||||
|
||||
it("works for ERC20 send transaction", () => {
|
||||
const msg: types.MsgExecuteContract = {
|
||||
type: "wasm/execute",
|
||||
value: {
|
||||
sender: "cosmos1h806c7khnvmjlywdrkdgk2vrayy2mmvf9rxk2r",
|
||||
contract: defaultErc20Tokens[0].contractAddress,
|
||||
msg: {
|
||||
transfer: {
|
||||
amount: "887878484",
|
||||
recipient: "cosmos1z7g5w84ynmjyg0kqpahdjqpj7yq34v3suckp0e",
|
||||
},
|
||||
},
|
||||
sent_funds: [],
|
||||
},
|
||||
};
|
||||
const tx: types.StdTx = {
|
||||
msg: [msg],
|
||||
memo: defaultMemo,
|
||||
fee: {
|
||||
amount: [
|
||||
{
|
||||
denom: "uatom",
|
||||
amount: "5000",
|
||||
},
|
||||
],
|
||||
gas: "200000",
|
||||
},
|
||||
signatures: [],
|
||||
};
|
||||
const unsigned = parseUnsignedTx(tx, testdata.chainId, defaultTokens, defaultErc20Tokens);
|
||||
assert(isSendTransaction(unsigned));
|
||||
expect(unsigned).toEqual({
|
||||
kind: "bcp/send",
|
||||
chainId: testdata.chainId,
|
||||
sender: "cosmos1h806c7khnvmjlywdrkdgk2vrayy2mmvf9rxk2r" as Address,
|
||||
recipient: "cosmos1z7g5w84ynmjyg0kqpahdjqpj7yq34v3suckp0e" as Address,
|
||||
amount: {
|
||||
quantity: "887878484",
|
||||
tokenTicker: "ASH" as TokenTicker,
|
||||
fractionalDigits: 5,
|
||||
},
|
||||
memo: defaultMemo,
|
||||
fee: defaultFee,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseSignedTx", () => {
|
||||
it("works", () => {
|
||||
expect(parseSignedTx(cosmoshub.tx.value, testdata.chainId, testdata.nonce, defaultTokens)).toEqual(
|
||||
testdata.signedTxJson,
|
||||
);
|
||||
expect(
|
||||
parseSignedTx(
|
||||
cosmoshub.tx.value,
|
||||
testdata.chainId,
|
||||
testdata.nonce,
|
||||
defaultTokens,
|
||||
defaultErc20Tokens,
|
||||
),
|
||||
).toEqual(testdata.signedTxJson);
|
||||
});
|
||||
});
|
||||
|
||||
@ -192,9 +295,15 @@ describe("decode", () => {
|
||||
transactionId: testdata.txId,
|
||||
log: '[{"msg_index":0,"success":true,"log":""}]',
|
||||
};
|
||||
expect(parseTxsResponseUnsigned(testdata.chainId, currentHeight, txsResponse, defaultTokens)).toEqual(
|
||||
expected,
|
||||
);
|
||||
expect(
|
||||
parseTxsResponseUnsigned(
|
||||
testdata.chainId,
|
||||
currentHeight,
|
||||
txsResponse,
|
||||
defaultTokens,
|
||||
defaultErc20Tokens,
|
||||
),
|
||||
).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
@ -216,7 +325,14 @@ describe("decode", () => {
|
||||
log: '[{"msg_index":0,"success":true,"log":""}]',
|
||||
};
|
||||
expect(
|
||||
parseTxsResponseSigned(testdata.chainId, currentHeight, testdata.nonce, txsResponse, defaultTokens),
|
||||
parseTxsResponseSigned(
|
||||
testdata.chainId,
|
||||
currentHeight,
|
||||
testdata.nonce,
|
||||
txsResponse,
|
||||
defaultTokens,
|
||||
defaultErc20Tokens,
|
||||
),
|
||||
).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
@ -19,8 +19,9 @@ import {
|
||||
UnsignedTransaction,
|
||||
} from "@iov/bcp";
|
||||
import { Decimal, Encoding } from "@iov/encoding";
|
||||
import BN from "bn.js";
|
||||
|
||||
import { BankTokens } from "./types";
|
||||
import { BankTokens, Erc20Token } from "./types";
|
||||
|
||||
const { fromBase64 } = Encoding;
|
||||
|
||||
@ -76,6 +77,7 @@ export function parseMsg(
|
||||
memo: string | undefined,
|
||||
chainId: ChainId,
|
||||
tokens: BankTokens,
|
||||
erc20Tokens: readonly Erc20Token[],
|
||||
): UnsignedTransaction {
|
||||
if (types.isMsgSend(msg)) {
|
||||
if (msg.value.amount.length !== 1) {
|
||||
@ -90,6 +92,34 @@ export function parseMsg(
|
||||
memo: memo,
|
||||
};
|
||||
return send;
|
||||
} else if (types.isMsgExecuteContract(msg)) {
|
||||
const matchingTokenContract = erc20Tokens.find(t => t.contractAddress === msg.value.contract);
|
||||
if (!matchingTokenContract) {
|
||||
return {
|
||||
chainId: chainId,
|
||||
kind: "bcp/unknown",
|
||||
};
|
||||
}
|
||||
|
||||
const recipient: string | undefined = (msg.value.msg as any).transfer?.recipient;
|
||||
if (!recipient) throw new Error("Could not read recipient");
|
||||
|
||||
const amount: string | undefined = (msg.value.msg as any).transfer?.amount;
|
||||
if (!amount) throw new Error("Could not read recipient");
|
||||
|
||||
const send: SendTransaction = {
|
||||
kind: "bcp/send",
|
||||
chainId: chainId,
|
||||
sender: msg.value.sender as Address,
|
||||
recipient: recipient as Address,
|
||||
amount: {
|
||||
quantity: new BN(amount).toString(),
|
||||
fractionalDigits: matchingTokenContract.fractionalDigits,
|
||||
tokenTicker: matchingTokenContract.ticker as TokenTicker,
|
||||
},
|
||||
memo: memo,
|
||||
};
|
||||
return send;
|
||||
} else {
|
||||
// Unknown transaction type
|
||||
const unknown = {
|
||||
@ -114,6 +144,7 @@ export function parseUnsignedTx(
|
||||
txValue: types.StdTx,
|
||||
chainId: ChainId,
|
||||
tokens: BankTokens,
|
||||
erc20Tokens: readonly Erc20Token[],
|
||||
): UnsignedTransaction {
|
||||
if (!types.isStdTx(txValue)) {
|
||||
throw new Error("Only StdTx is supported");
|
||||
@ -122,7 +153,7 @@ export function parseUnsignedTx(
|
||||
throw new Error("Only single-message transactions currently supported");
|
||||
}
|
||||
|
||||
const msg = parseMsg(txValue.msg[0], txValue.memo, chainId, tokens);
|
||||
const msg = parseMsg(txValue.msg[0], txValue.memo, chainId, tokens, erc20Tokens);
|
||||
const fee = parseFee(txValue.fee, tokens);
|
||||
|
||||
return {
|
||||
@ -137,10 +168,11 @@ export function parseSignedTx(
|
||||
chainId: ChainId,
|
||||
nonce: Nonce,
|
||||
tokens: BankTokens,
|
||||
erc20Tokens: readonly Erc20Token[],
|
||||
): SignedTransaction {
|
||||
const [primarySignature] = txValue.signatures.map(signature => decodeFullSignature(signature, nonce));
|
||||
return {
|
||||
transaction: parseUnsignedTx(txValue, chainId, tokens),
|
||||
transaction: parseUnsignedTx(txValue, chainId, tokens, erc20Tokens),
|
||||
signatures: [primarySignature],
|
||||
};
|
||||
}
|
||||
@ -150,10 +182,11 @@ export function parseTxsResponseUnsigned(
|
||||
currentHeight: number,
|
||||
response: TxsResponse,
|
||||
tokens: BankTokens,
|
||||
erc20Tokens: readonly Erc20Token[],
|
||||
): ConfirmedTransaction<UnsignedTransaction> {
|
||||
const height = parseInt(response.height, 10);
|
||||
return {
|
||||
transaction: parseUnsignedTx(response.tx.value, chainId, tokens),
|
||||
transaction: parseUnsignedTx(response.tx.value, chainId, tokens, erc20Tokens),
|
||||
height: height,
|
||||
confirmations: currentHeight - height + 1,
|
||||
transactionId: response.txhash as TransactionId,
|
||||
@ -167,10 +200,11 @@ export function parseTxsResponseSigned(
|
||||
nonce: Nonce,
|
||||
response: TxsResponse,
|
||||
tokens: BankTokens,
|
||||
erc20Tokens: readonly Erc20Token[],
|
||||
): ConfirmedAndSignedTransaction<UnsignedTransaction> {
|
||||
const height = parseInt(response.height, 10);
|
||||
return {
|
||||
...parseSignedTx(response.tx.value, chainId, nonce, tokens),
|
||||
...parseSignedTx(response.tx.value, chainId, nonce, tokens, erc20Tokens),
|
||||
height: height,
|
||||
confirmations: currentHeight - height + 1,
|
||||
transactionId: response.txhash as TransactionId,
|
||||
|
||||
7
packages/bcp/types/decode.d.ts
vendored
7
packages/bcp/types/decode.d.ts
vendored
@ -13,7 +13,7 @@ import {
|
||||
UnsignedTransaction,
|
||||
} from "@iov/bcp";
|
||||
import { Decimal } from "@iov/encoding";
|
||||
import { BankTokens } from "./types";
|
||||
import { BankTokens, Erc20Token } from "./types";
|
||||
export declare function decodePubkey(pubkey: types.PubKey): PubkeyBundle;
|
||||
export declare function decodeSignature(signature: string): SignatureBytes;
|
||||
export declare function decodeFullSignature(signature: types.StdSignature, nonce: number): FullSignature;
|
||||
@ -24,24 +24,28 @@ export declare function parseMsg(
|
||||
memo: string | undefined,
|
||||
chainId: ChainId,
|
||||
tokens: BankTokens,
|
||||
erc20Tokens: readonly Erc20Token[],
|
||||
): UnsignedTransaction;
|
||||
export declare function parseFee(fee: types.StdFee, tokens: BankTokens): Fee;
|
||||
export declare function parseUnsignedTx(
|
||||
txValue: types.StdTx,
|
||||
chainId: ChainId,
|
||||
tokens: BankTokens,
|
||||
erc20Tokens: readonly Erc20Token[],
|
||||
): UnsignedTransaction;
|
||||
export declare function parseSignedTx(
|
||||
txValue: types.StdTx,
|
||||
chainId: ChainId,
|
||||
nonce: Nonce,
|
||||
tokens: BankTokens,
|
||||
erc20Tokens: readonly Erc20Token[],
|
||||
): SignedTransaction;
|
||||
export declare function parseTxsResponseUnsigned(
|
||||
chainId: ChainId,
|
||||
currentHeight: number,
|
||||
response: TxsResponse,
|
||||
tokens: BankTokens,
|
||||
erc20Tokens: readonly Erc20Token[],
|
||||
): ConfirmedTransaction<UnsignedTransaction>;
|
||||
export declare function parseTxsResponseSigned(
|
||||
chainId: ChainId,
|
||||
@ -49,4 +53,5 @@ export declare function parseTxsResponseSigned(
|
||||
nonce: Nonce,
|
||||
response: TxsResponse,
|
||||
tokens: BankTokens,
|
||||
erc20Tokens: readonly Erc20Token[],
|
||||
): ConfirmedAndSignedTransaction<UnsignedTransaction>;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user