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 sing message support #1

Open
wants to merge 29 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
1340183
Merge pull request #1204 from near/dev
Sep 11, 2024
2fd7c12
Merge pull request #1220 from near/dev
gtsonevv Oct 23, 2024
ca6d30d
Remove @near-js/ aliasis from tsconfig.base.json.
kujtimprenku Nov 3, 2024
864fce7
Remove @near-js import and use the near-api-js directly.
kujtimprenku Nov 3, 2024
5fde290
Remove un-necessary peer dependency @near-js/providers.
kujtimprenku Nov 3, 2024
54ae22c
Bump borsh to v1.0.0 and remove the better-sqlite3
kujtimprenku Nov 3, 2024
a63530c
feat: add to devtools projet workflow
thisisjoshford Nov 22, 2024
6d80d51
feat(ledger): add sign message to ledger
JordiParraCrespo Dec 10, 2024
170480b
test(ledger): update test for ledger and ledger-client with sign message
JordiParraCrespo Dec 10, 2024
9e64910
fix(ledger): improve error message
JordiParraCrespo Dec 10, 2024
8c48c13
style(ledger): lint the code
JordiParraCrespo Dec 10, 2024
6611c19
Merge pull request #1229 from kujtimprenku/fix/mintbase-js-and-naj
gtsonevv Dec 13, 2024
5deac85
Merge pull request #1230 from kujtimprenku/fix/remove-near-js-individ…
gtsonevv Dec 13, 2024
37616ba
Merge pull request #1231 from kujtimprenku/feat/bump-borsh-dependency
gtsonevv Dec 13, 2024
8174d32
chore-bump-version-v8.9.15
gtsonevv Dec 13, 2024
b67dbba
Merge pull request #1268 from near/chore-bump-version-v8.9.15
gtsonevv Dec 13, 2024
f7ca952
Merge pull request #1269 from near/dev
gtsonevv Dec 13, 2024
9a0e570
Merge branch 'dev' of github.com:near/wallet-selector into HEAD
JordiParraCrespo Dec 19, 2024
8836b90
fix(ledger): update to new borsh version
JordiParraCrespo Dec 19, 2024
cf89e92
fix: define range of `near-api-js` versions as peerDependency instead…
denbite Dec 30, 2024
f172cdc
feat: meteor wallet in app selector
Elabar Jan 2, 2025
d7e5f73
chore: update readme
Elabar Jan 2, 2025
3a83dd0
chore: code linting
Elabar Jan 2, 2025
93ae2de
Merge pull request #1272 from near/update_peer_dependencies
denbite Jan 7, 2025
86655df
feat: use nonce instead of incremental promise id
Elabar Jan 9, 2025
5cfc978
feat: allow post message for all sites, add a source tag in message
Elabar Jan 9, 2025
2429bd6
feat: pass window location href
Elabar Jan 14, 2025
69ab20d
Merge pull request #1275 from Elabar/meteor-wallet-mobile-app
trechriron Jan 17, 2025
41030ba
Merge branch 'dev' of github.com:near/wallet-selector into ledger/fea…
JordiParraCrespo Jan 17, 2025
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
98 changes: 86 additions & 12 deletions packages/ledger/src/lib/ledger-client.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,19 @@ const createTransactionMock = () => {
);
};

const createSignMessageMock = () => {
/**
* This is a hex encoded payload that is sent to the Ledger device.
* message: "Makes it possible to authenticate users without having to add new access keys. This will improve UX, save money and will not increase the on-chain storage of the users' accounts./Makes it possible to authenticate users without having to add new access keys. This will improve UX, save money and will not increase the on-chain storage of the users' accounts./Makes it possible to authenticate users without having to add new access keys. This will improve UX, save money and will not increase the on-chain storage of the users' accounts.",
AgustinMJ marked this conversation as resolved.
Show resolved Hide resolved
* nonce: new Array(32).fill(42),
* recipient: "alice.near",
* callbackUrl: "myapp.com/callback",
*/
const hexEncodedPayload =
"180200004d616b657320697420706f737369626c6520746f2061757468656e74696361746520757365727320776974686f757420686176696e6720746f20616464206e657720616363657373206b6579732e20546869732077696c6c20696d70726f76652055582c2073617665206d6f6e657920616e642077696c6c206e6f7420696e63726561736520746865206f6e2d636861696e2073746f72616765206f662074686520757365727327206163636f756e74732e2f4d616b657320697420706f737369626c6520746f2061757468656e74696361746520757365727320776974686f757420686176696e6720746f20616464206e657720616363657373206b6579732e20546869732077696c6c20696d70726f76652055582c2073617665206d6f6e657920616e642077696c6c206e6f7420696e63726561736520746865206f6e2d636861696e2073746f72616765206f662074686520757365727327206163636f756e74732e2f4d616b657320697420706f737369626c6520746f2061757468656e74696361746520757365727320776974686f757420686176696e6720746f20616464206e657720616363657373206b6579732e20546869732077696c6c20696d70726f76652055582c2073617665206d6f6e657920616e642077696c6c206e6f7420696e63726561736520746865206f6e2d636861696e2073746f72616765206f662074686520757365727327206163636f756e74732e2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a0a000000616c6963652e6e65617201120000006d796170702e636f6d2f63616c6c6261636b";
return Buffer.from(hexEncodedPayload, "hex");
};

const createLedgerClient = (params: CreateLedgerClientParams = {}) => {
const client = mock<TransportWebHID>(params.client);
const transport = mock<Transport>(params.transport);
Expand All @@ -63,9 +76,7 @@ const createLedgerClient = (params: CreateLedgerClientParams = {}) => {
const {
LedgerClient,
CLA,
INS_SIGN,
INS_GET_APP_VERSION,
INS_GET_PUBLIC_KEY,
NEAR_INS,
P1_LAST,
P1_IGNORE,
P2_IGNORE,
Expand All @@ -80,9 +91,11 @@ const createLedgerClient = (params: CreateLedgerClientParams = {}) => {
parseDerivationPath,
constants: {
CLA,
INS_SIGN,
INS_GET_APP_VERSION,
INS_GET_PUBLIC_KEY,
INS_SIGN_TRANSACTION: NEAR_INS.SIGN_TRANSACTION,
INS_GET_APP_VERSION: NEAR_INS.GET_VERSION,
INS_GET_PUBLIC_KEY: NEAR_INS.GET_PUBLIC_KEY,
INS_NEP413_SIGN_MESSAGE: NEAR_INS.NEP413_SIGN_MESSAGE,
INS_NEP366_SIGN_DELEGATE_ACTION: NEAR_INS.NEP366_SIGN_DELEGATE_ACTION,
P1_LAST,
P1_IGNORE,
P2_IGNORE,
Expand Down Expand Up @@ -154,32 +167,93 @@ describe("sign", () => {
const data = nearAPI.transactions.encodeTransaction(transaction);

await client.connect();

const result = await client.sign({
data: Buffer.from(data),
derivationPath: "44'/397'/0'/0'/1'",
});

//Get version call
expect(transport.send).toHaveBeenNthCalledWith(
1,
constants.CLA,
constants.INS_GET_APP_VERSION,
constants.P1_IGNORE,
constants.P2_IGNORE
);

//Sign call
expect(transport.send).toHaveBeenNthCalledWith(
2,
constants.CLA,
constants.INS_SIGN_TRANSACTION,
constants.P1_LAST,
constants.P2_IGNORE,
expect.any(Buffer)
);

expect(transport.send).toHaveBeenCalledTimes(2);
expect(result).toEqual(Buffer.from([1]));
});
});

describe("signMessage", () => {
it("returns the signature", async () => {
const { client, transport, constants } = createLedgerClient({
transport: {
send: jest.fn().mockResolvedValue(Buffer.from([1, 2, 3])),
},
});

const data = createSignMessageMock();

await client.connect();

const result = await client.signMessage({
data,
derivationPath: "44'/397'/0'/0'/1'",
});

expect(transport.send).toHaveBeenCalledWith(
//Get version call
expect(transport.send).toHaveBeenNthCalledWith(
1,
constants.CLA,
constants.INS_GET_APP_VERSION,
constants.P1_IGNORE,
constants.P2_IGNORE
);
expect(transport.send).toHaveBeenCalledWith(

//Sign call 1
expect(transport.send).toHaveBeenNthCalledWith(
2,
constants.CLA,
constants.INS_SIGN,
constants.INS_NEP413_SIGN_MESSAGE,
constants.P1_IGNORE,
constants.P2_IGNORE,
expect.any(Buffer)
);
expect(transport.send).toHaveBeenCalledWith(

//Sign call 2
expect(transport.send).toHaveBeenNthCalledWith(
3,
constants.CLA,
constants.INS_SIGN,
constants.INS_NEP413_SIGN_MESSAGE,
constants.P1_IGNORE,
constants.P2_IGNORE,
expect.any(Buffer)
);

//Sign call 3
expect(transport.send).toHaveBeenNthCalledWith(
4,
constants.CLA,
constants.INS_NEP413_SIGN_MESSAGE,
constants.P1_LAST,
constants.P2_IGNORE,
expect.any(Buffer)
);
expect(transport.send).toHaveBeenCalledTimes(3);

expect(transport.send).toHaveBeenCalledTimes(4);
expect(result).toEqual(Buffer.from([1]));
});
});
Expand Down
66 changes: 52 additions & 14 deletions packages/ledger/src/lib/ledger-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,21 @@ import * as nearAPI from "near-api-js";
// - https://github.com/LedgerHQ/app-near/blob/master/workdir/app-near/src/constants.h

export const CLA = 0x80; // Always the same for Ledger.
export const INS_SIGN = 0x02; // Sign
export const INS_GET_PUBLIC_KEY = 0x04; // Get Public Key
export const INS_GET_APP_VERSION = 0x06; // Get App Version

export enum NEAR_INS {
GET_VERSION = 0x06,
GET_PUBLIC_KEY = 0x04,
GET_WALLET_ID = 0x05,
SIGN_TRANSACTION = 0x02,
NEP413_SIGN_MESSAGE = 0x07,
NEP366_SIGN_DELEGATE_ACTION = 0x08,
}

export const P1_LAST = 0x80; // End of Bytes to Sign (finalize)
export const P1_MORE = 0x00; // More bytes coming
export const P1_IGNORE = 0x00;
export const P2_IGNORE = 0x00;
export const CHUNK_SIZE = 250;

// Converts BIP32-compliant derivation path to a Buffer.
// More info here: https://github.com/LedgerHQ/ledger-live-common/blob/master/docs/derivation.md
Expand Down Expand Up @@ -46,10 +54,17 @@ interface GetPublicKeyParams {
}

interface SignParams {
data: Uint8Array;
data: Buffer;
derivationPath: string;
}

interface InternalSignParams extends SignParams {
ins:
| NEAR_INS.NEP366_SIGN_DELEGATE_ACTION
| NEAR_INS.NEP413_SIGN_MESSAGE
| NEAR_INS.SIGN_TRANSACTION;
}

interface EventMap {
disconnect: Error;
}
Expand Down Expand Up @@ -128,7 +143,7 @@ export class LedgerClient {

const res = await this.transport.send(
CLA,
INS_GET_APP_VERSION,
NEAR_INS.GET_VERSION,
P1_IGNORE,
P2_IGNORE
);
Expand All @@ -145,7 +160,7 @@ export class LedgerClient {

const res = await this.transport.send(
CLA,
INS_GET_PUBLIC_KEY,
NEAR_INS.GET_PUBLIC_KEY,
P2_IGNORE,
networkId,
parseDerivationPath(derivationPath)
Expand All @@ -154,27 +169,26 @@ export class LedgerClient {
return nearAPI.utils.serialize.base_encode(res.subarray(0, -2));
};

sign = async ({ data, derivationPath }: SignParams) => {
private internalSign = async ({
data,
derivationPath,
ins,
}: InternalSignParams) => {
if (!this.transport) {
throw new Error("Device not connected");
}

// NOTE: getVersion call resets state to avoid starting from partially filled buffer
await this.getVersion();

// 128 - 5 service bytes
const CHUNK_SIZE = 123;
const allData = Buffer.concat([
parseDerivationPath(derivationPath),
Buffer.from(data),
]);
const allData = Buffer.concat([parseDerivationPath(derivationPath), data]);

for (let offset = 0; offset < allData.length; offset += CHUNK_SIZE) {
const isLastChunk = offset + CHUNK_SIZE >= allData.length;

const response = await this.transport.send(
CLA,
INS_SIGN,
ins,
isLastChunk ? P1_LAST : P1_MORE,
P2_IGNORE,
Buffer.from(allData.subarray(offset, offset + CHUNK_SIZE))
Expand All @@ -187,4 +201,28 @@ export class LedgerClient {

throw new Error("Invalid data or derivation path");
};

sign = async ({ data, derivationPath }: SignParams) => {
return this.internalSign({
data,
derivationPath,
ins: NEAR_INS.SIGN_TRANSACTION,
});
};

signMessage = async ({ data, derivationPath }: SignParams) => {
return this.internalSign({
data,
derivationPath,
ins: NEAR_INS.NEP413_SIGN_MESSAGE,
});
};

signDelegateAction = async ({ data, derivationPath }: SignParams) => {
return this.internalSign({
data,
derivationPath,
ins: NEAR_INS.NEP366_SIGN_DELEGATE_ACTION,
});
};
}
49 changes: 49 additions & 0 deletions packages/ledger/src/lib/ledger.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,21 @@ const createLedgerWallet = async () => {
218, 45, 220, 10, 4,
])
),
signMessage: jest
.fn()
.mockResolvedValue(
Buffer.from(
"fn39aKtzVFDMJOYZiYTWBiE6HQh1QsmGbESQRMRS9dTidGcrDogXIarCvsMUfKsx79iDLicwjGCN7XO8fnYWDA==",
"base64"
)
),
});

jest.mock("@near-wallet-selector/core", () => {
return {
...jest.requireActual("@near-wallet-selector/core"),
verifySignature: jest.fn().mockReturnValue(true),
};
});

jest.mock("./ledger-client", () => {
Expand Down Expand Up @@ -174,6 +189,40 @@ describe("signAndSendTransactions", () => {
});
});

describe("signMessage", () => {
it("returns signature", async () => {
const accountId = "amirsaran.testnet";
const derivationPath = "44'/397'/0'/0'/1'";
const { wallet, ledgerClient, publicKey } = await createLedgerWallet();

await wallet.signIn({
accounts: [{ derivationPath, publicKey, accountId }],
contractId: "guest-book.testnet",
});

const message =
"Makes it possible to authenticate users without having to add new access keys. This will improve UX, save money and will not increase the on-chain storage of the users' accounts./Makes it possible to authenticate users without having to add new access keys. This will improve UX, save money and will not increase the on-chain storage of the users' accounts./Makes it possible to authenticate users without having to add new access keys. This will improve UX, save money and will not increase the on-chain storage of the users' accounts.";
AgustinMJ marked this conversation as resolved.
Show resolved Hide resolved
const nonce = Buffer.from(new Array(32).fill(42));
const recipient = "alice.near";
const callbackUrl = "myapp.com/callback";

const result = await wallet.signMessage!({
message,
nonce,
recipient,
callbackUrl,
});

expect(ledgerClient.signMessage).toHaveBeenCalled();

expect(result!.signature).toBeDefined();

expect(result!.accountId).toEqual(accountId);

expect(result!.publicKey).toEqual("ed25519:" + publicKey);
});
});

describe("getPublicKey", () => {
it("returns public key", async () => {
const accountId = "amirsaran.testnet";
Expand Down
Loading
Loading