Skip to content

Commit

Permalink
feat/1012: Improve Tx API in SDK (#1033)
Browse files Browse the repository at this point in the history
* feat: add Tx schema and type for inspecting transactions from SDK

* feat: continue hooking up new Tx type

* fix: serialize and deserialize properly

* feat: support all existing Tx

* feat: improve signing & broadcast API

* docs: regenerate docs for types & sdk
  • Loading branch information
jurevans authored Aug 28, 2024
1 parent 4f090ea commit 862de2a
Show file tree
Hide file tree
Showing 75 changed files with 1,003 additions and 1,185 deletions.
1 change: 1 addition & 0 deletions apps/extension/src/Setup/Ledger/LedgerConfirmation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export const LedgerConfirmation = (): JSX.Element => {
<ViewKeys
publicKeyAddress={account.publicKey}
transparentAccountAddress={account.address}
trimCharacters={35}
/>
<ActionButton size="lg" onClick={closeCurrentTab}>
Finish Setup
Expand Down
16 changes: 15 additions & 1 deletion apps/extension/src/background/approvals/handler.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { WrapperTxMsgValue } from "@namada/types";
import BigNumber from "bignumber.js";
import createMockInstance from "jest-create-mock-instance";
import {
ApproveConnectInterfaceMsg,
Expand Down Expand Up @@ -46,7 +48,19 @@ describe("approvals handler", () => {
};

const approveTxMsg = new ApproveSignTxMsg(
[{ txBytes: "", signingDataBytes: [""] }],
[
{
args: new WrapperTxMsgValue({
token: "",
feeAmount: BigNumber(0),
gasLimit: BigNumber(0),
chainId: "",
}),
hash: "",
bytes: "",
signingData: [],
},
],
"signer"
);

Expand Down
18 changes: 13 additions & 5 deletions apps/extension/src/background/approvals/service.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { WrapperTxMsgValue } from "@namada/types";
import { paramsToUrl } from "@namada/utils";
import { ChainsService } from "background/chains";
import { KeyRingService } from "background/keyring";
import { SdkService } from "background/sdk";
import { VaultService } from "background/vault";
import BigNumber from "bignumber.js";
import { ExtensionBroadcaster } from "extension";
import createMockInstance from "jest-create-mock-instance";
import { LocalStorage } from "storage";
Expand Down Expand Up @@ -210,16 +212,22 @@ describe("approvals service", () => {
it("should reject resolver", async () => {
const tabId = 1;
const signer = "signer";
// data expected to be base64-encoded
const txBytes = "dHhEYXRh"; // "txData"
const signingDataBytes = "c2lnbmluZ0RhdGE="; // "signingData"
// tx bytes expected to be base64-encoded
const bytes = "dHhEYXRh"; // "txData"

(keyRingService.queryAccountDetails as any).mockResolvedValue(() => ({}));

const signaturePromise = service.approveSignTx(signer, [
{
txBytes,
signingDataBytes: [signingDataBytes],
args: new WrapperTxMsgValue({
token: "",
feeAmount: BigNumber(0),
gasLimit: BigNumber(0),
chainId: "",
}),
hash: "",
bytes,
signingData: [],
},
]);

Expand Down
29 changes: 10 additions & 19 deletions apps/extension/src/background/approvals/service.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { fromBase64, toBase64 } from "@cosmjs/encoding";
import { toBase64 } from "@cosmjs/encoding";
import { v4 as uuid } from "uuid";
import browser, { Windows } from "webextension-polyfill";

import { BuiltTx } from "@heliax/namada-sdk/web";
import { KVStore } from "@namada/storage";
import { SignArbitraryResponse, TxDetails } from "@namada/types";
import { paramsToUrl } from "@namada/utils";
Expand All @@ -14,6 +13,7 @@ import { SdkService } from "background/sdk";
import { VaultService } from "background/vault";
import { ExtensionBroadcaster } from "extension";
import { LocalStorage } from "storage";
import { fromEncodedTx } from "utils";
import { EncodedTxData, PendingTx } from "./types";

export class ApprovalsService {
Expand Down Expand Up @@ -50,12 +50,10 @@ export class ApprovalsService {

const pendingTx: PendingTx = {
signer,
txs: txs.map(({ txBytes, signingDataBytes }) => ({
txBytes: fromBase64(txBytes),
signingDataBytes: signingDataBytes.map((bytes) => fromBase64(bytes)),
})),
txs: txs.map((encodedTx) => fromEncodedTx(encodedTx)),
checksums,
};

await this.txStore.set(msgId, pendingTx);

const url = `${browser.runtime.getURL(
Expand Down Expand Up @@ -122,16 +120,9 @@ export class ApprovalsService {
throw new Error(`Signing data for ${msgId} not found!`);
}

const txs = pendingTx.txs.map(({ txBytes, signingDataBytes }) => {
return new BuiltTx(
txBytes,
signingDataBytes.map((sdBytes) => [...sdBytes])
);
});

try {
const signedBytes: Uint8Array[] = [];
for await (const tx of txs) {
for await (const tx of pendingTx.txs) {
signedBytes.push(await this.keyRingService.sign(tx, signer));
}
resolvers.resolve(signedBytes);
Expand Down Expand Up @@ -165,8 +156,8 @@ export class ApprovalsService {
const { tx } = this.sdkService.getSdk();

try {
const signedTxs = pendingTx.txs.map(({ txBytes }, i) => {
return tx.appendSignature(txBytes, responseSign[i]);
const signedTxs = pendingTx.txs.map(({ bytes }, i) => {
return tx.appendSignature(bytes, responseSign[i]);
});
resolvers.resolve(signedTxs);
} catch (e) {
Expand Down Expand Up @@ -303,8 +294,8 @@ export class ApprovalsService {
}

const { tx } = this.sdkService.getSdk();
return pendingTx.txs.map(({ txBytes }) =>
tx.deserialize(txBytes, pendingTx.checksums || {})
return pendingTx.txs.map(({ bytes }) =>
tx.deserialize(bytes, pendingTx.checksums || {})
);
}

Expand All @@ -316,7 +307,7 @@ export class ApprovalsService {
}

if (pendingTx.txs) {
return pendingTx.txs.map(({ txBytes }) => toBase64(txBytes));
return pendingTx.txs.map(({ bytes }) => toBase64(bytes));
}
}

Expand Down
19 changes: 13 additions & 6 deletions apps/extension/src/background/approvals/types.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
import { TxData } from "@namada/types";
import { SigningDataProps, TxProps } from "@namada/types";

export type ApprovedOriginsStore = string[];

export type PendingTx = {
txs: TxData[];
txs: TxProps[];
signer: string;
checksums?: Record<string, string>;
};

export type PendingSignArbitrary = string;

// base64 encoded Tx data for use with postMessage
export type EncodedTxData = {
txBytes: string;
signingDataBytes: string[];
// base64 encoded Uint8Arrays for use with postMessage
export type EncodedSigningData = Pick<
SigningDataProps,
"publicKeys" | "threshold" | "feePayer" | "owner"
> & {
accountPublicKeysMap?: string;
};

export type EncodedTxData = Pick<TxProps, "args" | "hash"> & {
bytes: string;
signingData: EncodedSigningData[];
};
6 changes: 3 additions & 3 deletions apps/extension/src/background/keyring/keyring.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
Bip44Path,
DerivedAccount,
SignArbitraryResponse,
TxProps,
} from "@namada/types";
import { Result, assertNever, truncateInMiddle } from "@namada/utils";

Expand All @@ -19,7 +20,6 @@ import {
UtilityStore,
} from "./types";

import { BuiltTx } from "@namada/shared";
import { SdkService } from "background/sdk";
import { VaultService } from "background/vault";
import { KeyStore, KeyStoreType, SensitiveType, VaultStorage } from "storage";
Expand Down Expand Up @@ -544,14 +544,14 @@ export class KeyRing {
}

async sign(
builtTx: BuiltTx,
txProps: TxProps,
signer: string,
chainId: string
): Promise<Uint8Array> {
await this.vaultService.assertIsUnlocked();
const key = await this.getSigningKey(signer);
const { signing } = this.sdkService.getSdk();
return await signing.sign(builtTx, key, chainId);
return await signing.sign(txProps, key, chainId);
}

async signArbitrary(
Expand Down
6 changes: 3 additions & 3 deletions apps/extension/src/background/keyring/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import {
Bip44Path,
DerivedAccount,
SignArbitraryResponse,
TxProps,
} from "@namada/types";
import { Result, truncateInMiddle } from "@namada/utils";

import { BuiltTx } from "@namada/shared";
import { ChainsService } from "background/chains";
import { SdkService } from "background/sdk/service";
import { VaultService } from "background/vault";
Expand Down Expand Up @@ -167,9 +167,9 @@ export class KeyRingService {
return await IndexedDBKVStore.durabilityCheck();
}

async sign(builtTx: BuiltTx, signer: string): Promise<Uint8Array> {
async sign(txProps: TxProps, signer: string): Promise<Uint8Array> {
const { chainId } = await this.chainsService.getChain();
return await this._keyRing.sign(builtTx, signer, chainId);
return await this._keyRing.sign(txProps, signer, chainId);
}

async signArbitrary(
Expand Down
8 changes: 3 additions & 5 deletions apps/extension/src/provider/Namada.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { toBase64 } from "@cosmjs/encoding";
import {
Chain,
DerivedAccount,
Expand All @@ -10,6 +9,7 @@ import {
} from "@namada/types";
import { MessageRequester, Ports } from "router";

import { toEncodedTx } from "utils";
import {
ApproveConnectInterfaceMsg,
ApproveSignArbitraryMsg,
Expand Down Expand Up @@ -65,10 +65,8 @@ export class Namada implements INamada {
return await this.requester?.sendMessage(
Ports.Background,
new ApproveSignTxMsg(
txs.map(({ txBytes, signingDataBytes }) => ({
txBytes: toBase64(txBytes),
signingDataBytes: signingDataBytes.map((bytes) => toBase64(bytes)),
})),
// Encode all transactions for use with postMessage
txs.map((txProps) => toEncodedTx(txProps)),
signer,
checksums
)
Expand Down
4 changes: 2 additions & 2 deletions apps/extension/src/provider/Signer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
Signer as ISigner,
Namada,
SignArbitraryResponse,
TxData,
TxProps,
} from "@namada/types";

export class Signer implements ISigner {
Expand Down Expand Up @@ -44,7 +44,7 @@ export class Signer implements ISigner {
}

public async sign(
tx: TxData | TxData[],
tx: TxProps | TxProps[],
signer: string,
checksums?: Record<string, string>
): Promise<Uint8Array[] | undefined> {
Expand Down
31 changes: 28 additions & 3 deletions apps/extension/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { fromBase64, toBase64 } from "@cosmjs/encoding";
import { TxProps } from "@namada/types";
import { v5 as uuid } from "uuid";
import browser from "webextension-polyfill";

import { Result } from "@namada/utils";
import { EncodedTxData } from "background/approvals";

/**
* Query the current extension tab and close it
Expand Down Expand Up @@ -55,11 +58,33 @@ export const validatePrivateKey = (
): Result<null, PrivateKeyError> =>
privateKey.length > PRIVATE_KEY_MAX_LENGTH ?
Result.err({ t: "TooLong", maxLength: PRIVATE_KEY_MAX_LENGTH })
: !/^[0-9a-f]*$/.test(privateKey) ? Result.err({ t: "BadCharacter" })
: Result.ok(null);
: !/^[0-9a-f]*$/.test(privateKey) ? Result.err({ t: "BadCharacter" })
: Result.ok(null);

// Remove prefix from private key, which may be present when exporting keys from CLI
export const filterPrivateKeyPrefix = (privateKey: string): string =>
privateKey.length === PRIVATE_KEY_MAX_LENGTH + 2 ?
privateKey.replace(/^00/, "")
: privateKey;
: privateKey;

// Convert any Uint8Arrays in TxProps to string, and construct EncodedTxData
export const toEncodedTx = (txProps: TxProps): EncodedTxData => ({
...txProps,
bytes: toBase64(txProps.bytes),
signingData: txProps.signingData.map((sd) => ({
...sd,
accountPublicKeysMap:
sd.accountPublicKeysMap ? toBase64(sd.accountPublicKeysMap) : undefined,
})),
});

// Convert base64 strings back to Uint8Arrays in EncodedTxData to restore TxProps
export const fromEncodedTx = (encodedTxData: EncodedTxData): TxProps => ({
...encodedTxData,
bytes: fromBase64(encodedTxData.bytes),
signingData: encodedTxData.signingData.map((sd) => ({
...sd,
accountPublicKeysMap:
sd.accountPublicKeysMap ? fromBase64(sd.accountPublicKeysMap) : undefined,
})),
});
4 changes: 2 additions & 2 deletions apps/namadillo/src/App/Governance/SubmitVote.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { BuiltTx } from "@heliax/namada-sdk/web";
import {
ActionButton,
Modal,
Expand All @@ -7,6 +6,7 @@ import {
TickedRadioList,
} from "@namada/components";
import {
TxProps,
VoteProposalProps,
VoteType,
isVoteType,
Expand Down Expand Up @@ -80,7 +80,7 @@ export const WithProposalId: React.FC<{ proposalId: bigint }> = ({

const onCloseModal = (): void => navigate(-1);

const dispatchPendingNotification = (txs: BuiltTx[]): void => {
const dispatchPendingNotification = (txs: TxProps[]): void => {
dispatchNotification({
id: createNotificationId(txs),
type: "pending",
Expand Down
8 changes: 4 additions & 4 deletions apps/namadillo/src/atoms/notifications/functions.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { BuiltTx } from "@heliax/namada-sdk/web";
import { TxProps } from "@namada/types";

export const createNotificationId = (data?: BuiltTx | BuiltTx[]): string => {
export const createNotificationId = (data?: TxProps | TxProps[]): string => {
if (!data) return Date.now().toString();
if (Array.isArray(data)) {
return data.map((tx) => tx.tx_hash()).join(";");
return data.map((tx) => tx.hash).join(";");
}
return data.tx_hash();
return data.hash;
};
5 changes: 2 additions & 3 deletions apps/namadillo/src/hooks/useTransactionNotifications.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { BuiltTx } from "@heliax/namada-sdk/web";
import { Stack } from "@namada/components";
import { RedelegateMsgValue } from "@namada/types";
import { RedelegateMsgValue, TxProps } from "@namada/types";
import { shortenAddress } from "@namada/utils";
import { NamCurrency } from "App/Common/NamCurrency";
import {
Expand All @@ -22,7 +21,7 @@ const getTotalAmountFromTransactionList = (txs: TxWithAmount[]): BigNumber =>
}, new BigNumber(0));

const parseTxsData = <T extends TxWithAmount>(
tx: BuiltTx,
tx: TxProps,
data: T[]
): { id: string; total: BigNumber } => {
const id = createNotificationId(tx);
Expand Down
Loading

0 comments on commit 862de2a

Please sign in to comment.