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

## [Unreleased]

### Added

- Add `exportAccount` keyring method for base32 Stellar secret-seed export ([#187](https://github.com/MetaMask/internal-snaps/pull/187))

Comment thread
stanleyyconsensys marked this conversation as resolved.
## [0.1.0]

### Added
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Account management and SEP-43 signing entry points via `onKeyringRequest` → `K
| `AccountService` | `services/account` | Persist / derive / select accounts (snap state) |
| `OnChainAccountService` | `services/on-chain-account` | Snap-state snapshots for balances/assets; live activation for discovery |
| `TransactionService` | `services/transaction` | Local pending keyring txs for `listAccountTransactions` |
| `WalletService` | `services/wallet` | HD derive signing material; used by `exportAccount` (never persisted) |
| `SyncAccountsHandler` | `handlers/cronjob` | Scheduled after selection changes to refresh on-chain snapshots |

## Request / response
Expand Down Expand Up @@ -45,6 +46,7 @@ Requests are origin-checked, then dispatched to the methods below.
| `listAccountTransactions` | Paginated keyring transactions for the account | **Snap state** (pending / local txs via `TransactionService` — **not** Horizon history) |
| `discoverAccounts` | Derive BIP-44 address for index; return it only if activated on any requested scope | Derive locally; activation check is **live on-chain** (`NetworkService.getAccount`) |
| `resolveAccountAddress` | Given an address, return CAIP-10 if this snap owns it; else `null` (MetaMask may fall back) | **Snap state** (keyring account lookup by address) |
| `exportAccount` | Export the Stellar secret seed (`S…` strkey / base32). Only `encoding: "base32"` is supported | **Derived** via `WalletService` (never persisted) |
| `filterAccountChains` | Not implemented | Throws `MethodNotSupportedError` |
| `updateAccount` | Not implemented | Throws `MethodNotSupportedError` |
| `submitRequest` | [signTransaction.md](./signTransaction.md) · [signMessage.md](./signMessage.md) · [signAuthEntry.md](./signAuthEntry.md) | |
7 changes: 7 additions & 0 deletions packages/stellar-wallet-snap/snap.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@
"allowedOrigins": ["https://portfolio.metamask.io"],
"capabilities": {
"scopes": ["stellar:pubnet"],
"privateKey": {
"exportFormats": [
{
"encoding": "base32"
}
]
},
"bip44": {
"deriveIndex": true,
"deriveIndexRange": true,
Expand Down
20 changes: 20 additions & 0 deletions packages/stellar-wallet-snap/src/api/address.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { assert, StructError } from '@metamask/superstruct';
import {
StellarAddressOrContractStruct,
StellarAddressStruct,
StellarSecretKeyStruct,
} from './address';

describe('StellarAddressStruct', () => {
Expand All @@ -21,6 +22,25 @@ describe('StellarAddressStruct', () => {
});
});

describe('StellarSecretKeyStruct', () => {
it('accepts a valid Stellar secret seed', () => {
expect(() =>
assert(
'SAKICEVQLYWGSOJS4WW7HZJWAHZVEEBS527LHK5V4MLJALYKICQCJXMW',
StellarSecretKeyStruct,
),
).not.toThrow();
});

it.each([
'invalid-secret',
'GA7UCNSASSOPQYTRGJ2NC7TDBSXHMWK6JHS7AO6X2ZQAIQSTB5ELNFSO',
'',
])('rejects an invalid Stellar secret seed: "%s"', (secret) => {
expect(() => assert(secret, StellarSecretKeyStruct)).toThrow(StructError);
});
});

describe('StellarAddressOrContractStruct', () => {
it('accepts a valid Stellar address', () => {
expect(() =>
Expand Down
23 changes: 23 additions & 0 deletions packages/stellar-wallet-snap/src/api/address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,29 @@ export const StellarAddressStruct = refine(
},
);

/**
* Validation struct for a Stellar secret seed (`S…` strkey / base32).
*/
export const StellarSecretKeyStruct = refine(
nonempty(string()),
'stellar_secret_key',
(value: string) => {
try {
if (!StrKey.isValidEd25519SecretSeed(value)) {
return 'Invalid Stellar secret key';
}
return true;
} catch {
return 'Invalid Stellar secret key';
}
},
);

/**
* Type for a Stellar secret seed.
*/
export type StellarSecretKey = Infer<typeof StellarSecretKeyStruct>;

export const StellarAddressOrContractStruct = refine(
nonempty(string()),
'stellar_contract_or_address',
Expand Down
8 changes: 8 additions & 0 deletions packages/stellar-wallet-snap/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ import snapManifest from '../snap.manifest.json';
export const SUPPORTED_SCOPES =
snapManifest.initialPermissions['endowment:keyring'].capabilities.scopes;

/**
* Private-key export encoding supported by this snap.
*
* Always `base32` (Stellar `S…` strkey). Not read from the snap manifest:
* this snap does not support any other encoding.
*/
export const PRIVATE_KEY_EXPORT_ENCODING = 'base32' as const;

/**
* The base reserve for the Stellar network.
*
Expand Down
1 change: 1 addition & 0 deletions packages/stellar-wallet-snap/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ const keyringHandler = new KeyringHandler({
accountService,
onChainAccountService,
transactionService,
walletService,
handlers: keyringMethodHandlers,
});

Expand Down
61 changes: 60 additions & 1 deletion packages/stellar-wallet-snap/src/handlers/keyring/api.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { assert, StructError } from '@metamask/superstruct';
import { assert, create, StructError } from '@metamask/superstruct';

import { KnownCaip2ChainId } from '../../api';
import type { StellarKeyringAccount } from '../../services/account';
import { generateMockStellarKeyringAccounts } from '../../services/account/__mocks__/account.fixtures';
import {
CreateAccountOptionsStruct,
ExportAccountRequestStruct,
ResolveAccountAddressRequestStruct,
ListAccountTransactionsRequestStruct,
MultichainMethod,
Expand Down Expand Up @@ -586,3 +587,61 @@ describe('ListAccountTransactionsRequestStruct', () => {
);
});
});

describe('ExportAccountRequestStruct', () => {
it.each([
{
request: { accountId: account.id },
expected: {
accountId: account.id,
options: { type: 'private-key' as const, encoding: 'base32' as const },
},
},
{
request: {
accountId: account.id,
options: { type: 'private-key' as const },
},
expected: {
accountId: account.id,
options: { type: 'private-key' as const, encoding: 'base32' as const },
},
},
{
request: {
accountId: account.id,
options: { type: 'private-key' as const, encoding: 'base32' as const },
},
expected: {
accountId: account.id,
options: { type: 'private-key' as const, encoding: 'base32' as const },
},
},
])('accepts a valid exportAccount request', ({ request, expected }) => {
expect(create(request, ExportAccountRequestStruct)).toStrictEqual(expected);
});

it.each([
{ accountId: 'not-a-uuid' },
{
accountId: account.id,
options: { type: 'mnemonic', encoding: 'base32' },
},
{
accountId: account.id,
options: { type: 'private-key', encoding: 'utf-8' },
},
{
accountId: account.id,
options: { type: 'private-key', encoding: 'hexadecimal' },
},
{
accountId: account.id,
options: { type: 'private-key', encoding: 'base58' },
},
])('rejects an invalid exportAccount request', (request) => {
expect(() => assert(request, ExportAccountRequestStruct)).toThrow(
StructError,
);
});
});
30 changes: 30 additions & 0 deletions packages/stellar-wallet-snap/src/handlers/keyring/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
nullable,
enums,
refine,
defaulted,
} from '@metamask/superstruct';
import type { Infer } from '@metamask/superstruct';
import { base64 } from '@metamask/utils';
Expand All @@ -29,6 +30,7 @@ import { KnownCaip2ChainId, KnownCaip2ChainIdStruct } from '../../api/network';
import { Utf8StringStruct } from '../../api/string';
import { UuidStruct } from '../../api/uuid';
import { HashIdPreimageXdrStruct, XdrStruct } from '../../api/xdr';
import { PRIVATE_KEY_EXPORT_ENCODING } from '../../constants';
import { networkToCaip2ChainId } from '../../services/network/utils';

/** JSON-RPC methods supported by this snap's multichain keyring. */
Expand Down Expand Up @@ -306,6 +308,29 @@ export const SignAuthEntryResponseStruct = union([
*/
export const GetAccountRequestStruct = UuidStruct;

/**
* Validation struct for the exportAccount request.
*
* Only {@link PRIVATE_KEY_EXPORT_ENCODING} (`base32`) is accepted. Missing
* `options` or `encoding` default to that encoding.
*/
export const ExportAccountRequestStruct = object({
accountId: UuidStruct,
options: defaulted(
object({
type: literal('private-key'),
encoding: defaulted(
enums([PRIVATE_KEY_EXPORT_ENCODING]),
PRIVATE_KEY_EXPORT_ENCODING,
),
}),
{
type: 'private-key' as const,
encoding: PRIVATE_KEY_EXPORT_ENCODING,
},
),
});

/**
* Validation struct for the deleteAccount request.
*/
Expand Down Expand Up @@ -349,6 +374,11 @@ export type ResolveAccountAddressJsonRpcRequest = Infer<
*/
export type GetAccountRequest = Infer<typeof GetAccountRequestStruct>;

/**
* Type for the exportAccount request.
*/
export type ExportAccountRequest = Infer<typeof ExportAccountRequestStruct>;

/**
* Type for the deleteAccount request.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ import { StellarSnapException } from '../../utils/errors';

export class KeyringException extends StellarSnapException {}

/**
* Thrown when private-key export fails. Messages must not include the secret.
*/
export class ExportAccountException extends KeyringException {}

/**
* SEP-43 error codes.
*
Expand Down
Loading
Loading