Skip to content
Open
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
1 change: 1 addition & 0 deletions packages/stellar-wallet-snap/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Add `signProofOfOwnershipBatch` for signing multiple proof-of-ownership messages in one request. ([#267](https://github.com/MetaMask/internal-snaps/pull/267))
- Add `signProofOfOwnership` client request for silent proof-of-ownership signing (SEP-0053) ([#186](https://github.com/MetaMask/internal-snaps/pull/186))
- Add `exportAccount` keyring method for base32 Stellar secret-seed export ([#187](https://github.com/MetaMask/internal-snaps/pull/187))
- Add `TrustlineExceedLimitException` for send simulation when a payment would exceed the destination trustline limit (previously a generic `TransactionValidationException`) ([#185](https://github.com/MetaMask/internal-snaps/pull/185))
Expand Down
2 changes: 1 addition & 1 deletion packages/stellar-wallet-snap/snap.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"url": "https://github.com/MetaMask/internal-snaps.git"
},
"source": {
"shasum": "df2jwrVqYLs741TWeyLYKABatgBkFCEDWJdwpnNwiXw=",
"shasum": "SHnUG47ZILT1CsFbGHOFPRyGwRTlXQwuuMiX/U4pjMs=",
"location": {
"npm": {
"filePath": "dist/bundle.js",
Expand Down
9 changes: 9 additions & 0 deletions packages/stellar-wallet-snap/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { OnAddressInputHandler } from './handlers/clientRequest/onAddressInput';
import { OnAmountInputHandler } from './handlers/clientRequest/onAmountInput';
import { SignAndSendTransactionHandler } from './handlers/clientRequest/signAndSendTransaction';
import { SignProofOfOwnershipHandler } from './handlers/clientRequest/signProofOfOwnership';
import { SignProofOfOwnershipBatchHandler } from './handlers/clientRequest/signProofOfOwnershipBatch';
import type { ICronjobRequestHandler } from './handlers/cronjob/api';
import { BackgroundEventMethod } from './handlers/cronjob/api';
import {
Expand Down Expand Up @@ -290,6 +291,12 @@ const signProofOfOwnershipHandler = new SignProofOfOwnershipHandler({
accountResolver,
});

const signProofOfOwnershipBatchHandler = new SignProofOfOwnershipBatchHandler({
logger,
accountService,
walletService,
});

const clientRequestMethodHandlers: Record<
ClientRequestMethod,
IClientRequestHandler
Expand All @@ -301,6 +308,8 @@ const clientRequestMethodHandlers: Record<
[ClientRequestMethod.SignAndSendTransaction]: signAndSendTransactionHandler,
[ClientRequestMethod.ComputeFee]: computeFeeHandler,
[ClientRequestMethod.SignProofOfOwnership]: signProofOfOwnershipHandler,
[ClientRequestMethod.SignProofOfOwnershipBatch]:
signProofOfOwnershipBatchHandler,
};

const clientRequestHandler = new ClientRequestHandler({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import {
ConfirmSendJsonRpcResponseStruct,
SignAndSendTransactionJsonRpcRequestStruct,
SignAndSendTransactionJsonRpcResponseStruct,
SignProofOfOwnershipBatchJsonRpcRequestStruct,
SignProofOfOwnershipBatchJsonRpcResponseStruct,
SignProofOfOwnershipJsonRpcRequestStruct,
SignProofOfOwnershipJsonRpcResponseStruct,
} from './api';
Expand Down Expand Up @@ -1072,3 +1074,90 @@ describe('SignProofOfOwnershipJsonRpcResponseStruct', () => {
).toThrow(StructError);
});
});

describe('SignProofOfOwnershipBatchJsonRpcRequestStruct', () => {
const nonce = 'a1b2c3d4e5f6789012345678';

it('accepts a valid signProofOfOwnershipBatch request', () => {
expect(() =>
assert(
{
jsonrpc: '2.0',
id: 1,
method: ClientRequestMethod.SignProofOfOwnershipBatch,
params: {
items: [
{
accountId,
message: `metamask:proof-of-ownership:${nonce}:${stellarAddress}`,
},
],
},
},
SignProofOfOwnershipBatchJsonRpcRequestStruct,
),
).not.toThrow();
});

it.each([
{
method: ClientRequestMethod.SignProofOfOwnership,
params: { items: [] },
},
{
method: ClientRequestMethod.SignProofOfOwnershipBatch,
params: {},
},
{
method: ClientRequestMethod.SignProofOfOwnershipBatch,
params: { items: [{ accountId }] },
},
{
method: ClientRequestMethod.SignProofOfOwnershipBatch,
params: { items: [{ accountId: 'not-a-uuid', message: 'message' }] },
},
])(
'rejects an invalid signProofOfOwnershipBatch request',
({ method, params }) => {
expect(() =>
assert(
{ jsonrpc: '2.0', id: 1, method, params },
SignProofOfOwnershipBatchJsonRpcRequestStruct,
),
).toThrow(StructError);
},
);
});

describe('SignProofOfOwnershipBatchJsonRpcResponseStruct', () => {
it('accepts per-item success and error results', () => {
expect(() =>
assert(
{
results: [
{
accountId,
signature: `0x${'ab'.repeat(64)}`,
},
{
accountId: '22222222-2222-4222-8222-222222222222',
error: 'Account not found',
},
],
},
SignProofOfOwnershipBatchJsonRpcResponseStruct,
),
).not.toThrow();
});

it.each([
{},
{ results: [{ accountId, signature: 'not-a-signature' }] },
{ results: [{ accountId, error: 123 }] },
{ results: [{ accountId: 'not-a-uuid', error: 'bad id' }] },
])('rejects an invalid signProofOfOwnershipBatch response', (response) => {
expect(() =>
assert(response, SignProofOfOwnershipBatchJsonRpcResponseStruct),
).toThrow(StructError);
});
});
80 changes: 78 additions & 2 deletions packages/stellar-wallet-snap/src/handlers/clientRequest/api.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { AssetStruct, FeeType } from '@metamask/keyring-api';
import { UuidStruct } from '@metamask/snap-networks-utils';
import {
ProofOfOwnershipBatchErrorStruct as SignProofOfOwnershipBatchErrorStruct,
ProofOfOwnershipBatchRequestParamsStruct,
UuidStruct,
} from '@metamask/snap-networks-utils';
import type { Infer } from '@metamask/superstruct';
import {
enums,
Expand Down Expand Up @@ -39,6 +43,19 @@ import {
import { isSep41Id } from '../../utils';
import { parseProofOfOwnershipMessage } from './utils';

/**
* Validation struct for one signProofOfOwnershipBatch request item.
*
* Messages are validated inside the handler so invalid proof messages can be
* returned as per-item errors instead of rejecting the whole batch.
*/
export { ProofOfOwnershipBatchRequestItemStruct as SignProofOfOwnershipBatchJsonRpcRequestItemStruct } from '@metamask/snap-networks-utils';

/**
* Validation struct for one failed signProofOfOwnershipBatch result.
*/
export { ProofOfOwnershipBatchErrorStruct as SignProofOfOwnershipBatchErrorStruct } from '@metamask/snap-networks-utils';

/**
* Enum for the client request method.
*/
Expand All @@ -55,6 +72,11 @@ export const ClientRequestMethod = {
* SIP-31 client-only.
*/
SignProofOfOwnership: 'signProofOfOwnership',
/**
* Silent batch proof-of-ownership signing for
* `@metamask/profile-metrics-controller`. SIP-31 client-only.
*/
SignProofOfOwnershipBatch: 'signProofOfOwnershipBatch',
/** -------------------------------- Stellar Specific -------------------------------- */
ChangeTrustOpt: 'changeTrustOpt',
} as const;
Expand Down Expand Up @@ -409,6 +431,12 @@ export const ProofOfOwnershipMessageStruct = refine(
},
);

/**
* Validation struct for a 64-byte value encoded as lowercase hex with a leading
* `0x` prefix.
*/
export const SixtyFourByte0xHexStruct = pattern(string(), /^0x[0-9a-f]{128}$/u);

/**
* Validation struct for the signProofOfOwnership JSON-RPC request.
* Coerces `nonce` and `address` from `message` (clients send only accountId + message).
Expand Down Expand Up @@ -453,7 +481,41 @@ export const SignProofOfOwnershipJsonRpcRequestStruct = coerce(
* The `0x` prefix is not part of the 64-byte signature length.
*/
export const SignProofOfOwnershipJsonRpcResponseStruct = object({
signature: pattern(string(), /^0x[0-9a-f]{128}$/u),
signature: SixtyFourByte0xHexStruct,
});

/**
* Validation struct for the signProofOfOwnershipBatch JSON-RPC request.
*/
export const SignProofOfOwnershipBatchJsonRpcRequestStruct = assign(
JsonRpcRequestStruct,
object({
method: literal(ClientRequestMethod.SignProofOfOwnershipBatch),
params: ProofOfOwnershipBatchRequestParamsStruct,
}),
);

/**
* Validation struct for one successful signProofOfOwnershipBatch result.
*/
export const SignProofOfOwnershipBatchSuccessStruct = object({
accountId: UuidStruct,
signature: SixtyFourByte0xHexStruct,
});

/**
* Validation struct for one signProofOfOwnershipBatch result.
*/
export const SignProofOfOwnershipBatchItemResponseStruct = union([
SignProofOfOwnershipBatchSuccessStruct,
SignProofOfOwnershipBatchErrorStruct,
]);

/**
* Validation struct for the signProofOfOwnershipBatch JSON-RPC response.
*/
export const SignProofOfOwnershipBatchJsonRpcResponseStruct = object({
results: array(SignProofOfOwnershipBatchItemResponseStruct),
});

/**
Expand Down Expand Up @@ -561,3 +623,17 @@ export type SignProofOfOwnershipJsonRpcRequest = Infer<
export type SignProofOfOwnershipJsonRpcResponse = Infer<
typeof SignProofOfOwnershipJsonRpcResponseStruct
>;

/**
* Type for the signProofOfOwnershipBatch JSON-RPC request.
*/
export type SignProofOfOwnershipBatchJsonRpcRequest = Infer<
typeof SignProofOfOwnershipBatchJsonRpcRequestStruct
>;

/**
* Type for the signProofOfOwnershipBatch JSON-RPC response.
*/
export type SignProofOfOwnershipBatchJsonRpcResponse = Infer<
typeof SignProofOfOwnershipBatchJsonRpcResponseStruct
>;
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './changeTrustOpt';
export * from './clientRequest';
export * from './api';
export * from './signProofOfOwnershipBatch';
export type { IClientRequestHandler } from './base';
Loading