Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
8492fe8
feat(stellar-wallet-snap): Add `exportAccount` keyring API
stanleyyconsensys Aug 25, 2026
80a55e6
chore: update change log
stanleyyconsensys Aug 25, 2026
f2fa909
chore: update chg log
stanleyyconsensys Aug 25, 2026
2580da2
chore: bump snap sdk
stanleyyconsensys Aug 27, 2026
2f88e92
chore: update shasum
stanleyyconsensys Aug 27, 2026
9eb1740
chore: update version
stanleyyconsensys Aug 27, 2026
5097eaf
chore: update platformVersion
stanleyyconsensys Aug 27, 2026
5402d9b
chore: update keyring version
stanleyyconsensys Aug 27, 2026
b36fa8f
chore: update shasum
stanleyyconsensys Aug 27, 2026
44c9de6
chore: bump more package
stanleyyconsensys Aug 27, 2026
4bdce67
Merge branch 'bump/snap-sdk' into feat/stellar/export-pk
stanleyyconsensys Aug 27, 2026
4d9d49a
chore: update shasum
stanleyyconsensys Aug 27, 2026
f70542e
Merge branch 'main' into bump/snap-sdk
stanleyyconsensys Aug 28, 2026
fa54740
chore: update shasum
stanleyyconsensys Aug 28, 2026
060d142
chore: add change log
stanleyyconsensys Aug 28, 2026
9273c63
chore: update change log
stanleyyconsensys Aug 28, 2026
7e5dd77
chore: update change log
stanleyyconsensys Aug 28, 2026
ac06bc2
Merge branch 'main' into bump/snap-sdk
stanleyyconsensys Aug 28, 2026
8af3d98
chore: update snap sdk
stanleyyconsensys Aug 28, 2026
07fb687
Merge branch 'bump/snap-sdk' of https://github.com/MetaMask/internal-…
stanleyyconsensys Aug 28, 2026
89cfa7a
Merge branch 'main' into bump/snap-sdk
stanleyyconsensys Aug 31, 2026
dc0688b
chore: update plaform version
stanleyyconsensys Aug 31, 2026
65369a2
Merge branch 'main' into bump/snap-sdk
stanleyyconsensys Aug 31, 2026
dad3a81
Merge branch 'main' into bump/snap-sdk
stanleyyconsensys Sep 1, 2026
fccbd57
Merge branch 'main' into feat/stellar/export-pk
stanleyyconsensys Sep 1, 2026
0a94381
chore: use sensitive string
stanleyyconsensys Sep 1, 2026
0a9332f
Merge branch 'bump/snap-sdk' into feat/stellar/export-pk
stanleyyconsensys Sep 1, 2026
d20fa62
chore: rollback platform version
stanleyyconsensys Sep 1, 2026
4e0315b
Update CHANGELOG.md
stanleyyconsensys Sep 1, 2026
2505ff7
Merge branch 'main' into feat/stellar/export-pk
stanleyyconsensys Sep 1, 2026
d5c3a8d
Update CHANGELOG.md
stanleyyconsensys Sep 1, 2026
f0b6049
Update CHANGELOG.md
stanleyyconsensys Sep 1, 2026
6d810b8
Merge branch 'main' into feat/stellar/export-pk
stanleyyconsensys Sep 2, 2026
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 `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))
- Add `@metamask/snap-networks-utils` `^1.0.0` ([#182](https://github.com/MetaMask/internal-snaps/pull/182))
- Use the shared `Logger`
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) | |
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
27 changes: 25 additions & 2 deletions packages/stellar-wallet-snap/src/api/address.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { refine, string, nonempty } from '@metamask/superstruct';
import { refine, string, nonempty, sensitive } from '@metamask/superstruct';
import type { Infer } from '@metamask/superstruct';
import { StrKey } from '@stellar/stellar-sdk';

Expand All @@ -7,7 +7,7 @@ import { StrKey } from '@stellar/stellar-sdk';
* We only support non-muxed addresses.
*/
export const StellarAddressStruct = refine(
nonempty(string()),
nonempty(sensitive(string())),
'stellar_address',
(value: string) => {
try {
Expand All @@ -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 @@ -309,6 +311,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 @@ -352,6 +377,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