Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Ledger Handle Public Key Linked to Multiple Accounts #330

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions packages/core/docs/api/wallet.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ Returns meta information about the wallet such as `name`, `description`, `iconUr
- `params` (`object`)
- `contractId` (`string`): Account ID of the Smart Contract.
- `methodNames` (`Array<string>?`): Specify limited access to particular methods on the Smart Contract.
- `derivationPaths` (`Array<string>?`): Required for hardware wallets (e.g. Ledger). This is a list of derivation paths linked to public keys on your device.
- `accounts` (`Array<{derivationPath: string, publicKey: string, accountId: string}>?`): Required for hardware wallets (e.g. Ledger). This is a list of `accounts` linked to public keys on your device.

**Returns**

Expand Down Expand Up @@ -114,9 +114,17 @@ Programmatically sign in. Hardware wallets (e.g. Ledger) require `derivationPath
// Ledger
(async () => {
const wallet = await selector.wallet("ledger");
const derivationPath = "44'/397'/0'/0'/1'";
const publicKey = await wallet.getPublicKey(derivationPath);
const accountId = "youraccountid.testnet"

const accounts = await wallet.signIn({
contractId: "test.testnet",
derivationPaths: ["44'/397'/0'/0'/1'"],
accounts: [{
derivationPath,
publicKey,
accountId
}],
});
})();

Expand Down
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export type {
HardwareWalletSignInParams,
HardwareWalletBehaviour,
HardwareWallet,
HardwareWalletAccount,
BridgeWalletMetadata,
BridgeWalletBehaviour,
BridgeWallet,
Expand Down
12 changes: 10 additions & 2 deletions packages/core/src/lib/wallet/wallet.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,14 +120,22 @@ export type InjectedWallet = BaseWallet<

export type HardwareWalletMetadata = BaseWalletMetadata;

export interface HardwareWalletAccount {
derivationPath: string;
publicKey: string;
accountId: string;
}

export interface HardwareWalletSignInParams extends SignInParams {
derivationPaths: Array<string>;
accounts: Array<HardwareWalletAccount>;
}

export type HardwareWalletBehaviour = Modify<
BaseWalletBehaviour,
{ signIn(params: HardwareWalletSignInParams): Promise<Array<Account>> }
>;
> & {
getPublicKey(derivationPath: string): Promise<string>;
};

export type HardwareWallet = BaseWallet<
"hardware",
Expand Down
31 changes: 17 additions & 14 deletions packages/ledger/src/lib/ledger.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,16 +100,12 @@ describe("connect", () => {
it("signs in", async () => {
const accountId = "amirsaran.testnet";
const derivationPath = "44'/397'/0'/0'/1'";
const { wallet, ledgerClient, storage, publicKey } =
await createLedgerWallet();
const { wallet, storage, publicKey } = await createLedgerWallet();
await wallet.signIn({
derivationPaths: [derivationPath],
accounts: [{ derivationPath, publicKey, accountId }],
contractId: "guest-book.testnet",
});

expect(ledgerClient.isConnected).toHaveBeenCalledTimes(1);
expect(ledgerClient.connect).toHaveBeenCalledTimes(1);
expect(ledgerClient.getPublicKey).toHaveBeenCalledTimes(1);
expect(storage.setItem).toHaveBeenCalledWith("accounts", [
{
accountId,
Expand All @@ -123,9 +119,10 @@ describe("connect", () => {
describe("getAccounts", () => {
it("returns account objects", async () => {
const accountId = "amirsaran.testnet";
const { wallet } = await createLedgerWallet();
const derivationPath = "44'/397'/0'/0'/1'";
const { wallet, publicKey } = await createLedgerWallet();
await wallet.signIn({
derivationPaths: ["44'/397'/0'/0'/1'"],
accounts: [{ derivationPath, publicKey, accountId }],
contractId: "guest-book.testnet",
});
const result = await wallet.getAccounts();
Expand All @@ -140,9 +137,11 @@ describe("getAccounts", () => {

describe("signAndSendTransaction", () => {
it("signs and sends transaction", async () => {
const { wallet, ledgerClient } = await createLedgerWallet();
const accountId = "amirsaran.testnet";
const derivationPath = "44'/397'/0'/0'/1'";
const { wallet, ledgerClient, publicKey } = await createLedgerWallet();
await wallet.signIn({
derivationPaths: ["44'/397'/0'/0'/1'"],
accounts: [{ derivationPath, publicKey, accountId }],
contractId: "guest-book.testnet",
});
await wallet.signAndSendTransaction({
Expand All @@ -156,9 +155,11 @@ describe("signAndSendTransaction", () => {

describe("signAndSendTransactions", () => {
it("signs and sends only one transaction", async () => {
const { wallet, ledgerClient } = await createLedgerWallet();
const accountId = "amirsaran.testnet";
const derivationPath = "44'/397'/0'/0'/1'";
const { wallet, ledgerClient, publicKey } = await createLedgerWallet();
await wallet.signIn({
derivationPaths: ["44'/397'/0'/0'/1'"],
accounts: [{ derivationPath, publicKey, accountId }],
contractId: "guest-book.testnet",
});
const transactions: Array<Transaction> = [
Expand All @@ -176,9 +177,11 @@ describe("signAndSendTransactions", () => {
});

it("signs and sends multiple transactions", async () => {
const { wallet, ledgerClient } = await createLedgerWallet();
const accountId = "amirsaran.testnet";
const derivationPath = "44'/397'/0'/0'/1'";
const { wallet, ledgerClient, publicKey } = await createLedgerWallet();
await wallet.signIn({
derivationPaths: ["44'/397'/0'/0'/1'"],
accounts: [{ derivationPath, publicKey, accountId }],
contractId: "guest-book.testnet",
});
const transactions: Array<Transaction> = [
Expand Down
58 changes: 12 additions & 46 deletions packages/ledger/src/lib/ledger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,6 @@ interface ValidateAccessKeyParams {
publicKey: string;
}

interface GetAccountIdFromPublicKeyParams {
publicKey: string;
}

interface LedgerState {
client: LedgerClient;
accounts: Array<LedgerAccount>;
Expand Down Expand Up @@ -157,28 +153,6 @@ const Ledger: WalletBehaviourFactory<HardwareWallet> = async ({
);
};

const getAccountIdFromPublicKey = async ({
publicKey,
}: GetAccountIdFromPublicKeyParams): Promise<string> => {
const response = await fetch(
`${options.network.indexerUrl}/publicKey/ed25519:${publicKey}/accounts`
);

if (!response.ok) {
throw new Error("Failed to get account id from public key");
}

const accountIds = await response.json();

if (!Array.isArray(accountIds) || !accountIds.length) {
throw new Error(
"Failed to find account linked for public key: " + publicKey
);
}

return accountIds[0];
};

const transformTransactions = (
transactions: Array<Optional<Transaction, "signerId" | "receiverId">>
): Array<Transaction> => {
Expand All @@ -199,30 +173,17 @@ const Ledger: WalletBehaviourFactory<HardwareWallet> = async ({
};

return {
async signIn({ derivationPaths }) {
async signIn({ accounts }) {
const existingAccounts = getAccounts();

if (existingAccounts.length) {
return existingAccounts;
}

if (!derivationPaths.length) {
throw new Error("Invalid derivation paths");
}

// Note: Connection must be triggered by user interaction.
await connectLedgerDevice();

const accounts: Array<LedgerAccount> = [];

for (let i = 0; i < derivationPaths.length; i += 1) {
const derivationPath = derivationPaths[i];
const publicKey = await _state.client.getPublicKey({ derivationPath });
const accountId = await getAccountIdFromPublicKey({ publicKey });
const ledgerAccounts: Array<LedgerAccount> = [];

if (accounts.some((x) => x.accountId === accountId)) {
throw new Error("Duplicate account id: " + accountId);
}
for (let i = 0; i < accounts.length; i++) {
const { derivationPath, accountId, publicKey } = accounts[i];

const accessKey = await validateAccessKey({ accountId, publicKey });

Expand All @@ -232,15 +193,15 @@ const Ledger: WalletBehaviourFactory<HardwareWallet> = async ({
);
}

accounts.push({
ledgerAccounts.push({
accountId,
derivationPath,
publicKey,
});
}

await storage.setItem(STORAGE_ACCOUNTS, accounts);
_state.accounts = accounts;
await storage.setItem(STORAGE_ACCOUNTS, ledgerAccounts);
_state.accounts = ledgerAccounts;

return getAccounts();
},
Expand Down Expand Up @@ -290,6 +251,11 @@ const Ledger: WalletBehaviourFactory<HardwareWallet> = async ({
signedTransactions.map((signedTx) => provider.sendTransaction(signedTx))
);
},
async getPublicKey(derivationPath: string) {
await connectLedgerDevice();

return await _state.client.getPublicKey({ derivationPath });
},
};
};

Expand Down
Loading