Skip to content

Commit 25f0b4a

Browse files
feat: add confirmSend RPC (#76)
## Explanation Adds a new confirmSend client-request/RPC flow for Stellar “Unified Non‑EVM Send”, including a dedicated confirmation UI and handler wiring so a client can validate, confirm, sign, submit, and track a send transaction. <!-- Thanks for your contribution! Take a moment to answer these questions so that reviewers have the information they need to properly understand your changes: * What is the current state of things and why does it need to change? * What is the solution your changes offer and how does it work? * Are there any changes whose purpose might not obvious to those unfamiliar with the domain? * If your primary goal was to update one package but you found you had to update another one along the way, why did you do so? * If you had to upgrade a dependency, why did you do so? --> ## References <!-- Are there any issues that this pull request is tied to? Are there other links that reviewers should consult to understand these changes better? Are there client or consumer pull requests to adopt any breaking changes? For example: * Fixes #12345 * Related to #67890 --> ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've communicated my changes to consumers by [updating changelogs for packages I've changed](https://github.com/MetaMask/core/tree/main/docs/processes/updating-changelogs.md) - [ ] I've introduced [breaking changes](https://github.com/MetaMask/core/tree/main/docs/processes/breaking-changes.md) in this PR and have prepared draft pull requests for clients and consumer packages to resolve them
1 parent dc63d48 commit 25f0b4a

15 files changed

Lines changed: 1658 additions & 64 deletions

File tree

packages/site/src/pages/index.tsx

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,11 @@ const Index = () => {
238238
const [trustlineOutput, setTrustlineOutput] = useState<string | null>(null);
239239
const [trustlineLimit, setTrustlineLimit] = useState('');
240240

241+
const [sendAssetId, setSendAssetId] = useState('stellar:testnet/slip44:148');
242+
const [sendToAddress, setSendToAddress] = useState('');
243+
const [sendAmount, setSendAmount] = useState('1');
244+
const [sendOutput, setSendOutput] = useState<string | null>(null);
245+
241246
const isMetaMaskReady = isLocalSnap(defaultSnapOrigin)
242247
? isFlask
243248
: snapsDetected;
@@ -546,6 +551,128 @@ const Index = () => {
546551
}
547552
};
548553

554+
const invokeClientRequest = async (
555+
method: 'confirmSend' | 'onAmountInput' | 'onAddressInput',
556+
params: Record<string, unknown>,
557+
): Promise<unknown> =>
558+
invokeSnap({
559+
method: `stellar_${method}`,
560+
params: {
561+
jsonrpc: '2.0',
562+
id: crypto.randomUUID(),
563+
method,
564+
params,
565+
},
566+
});
567+
568+
const handleValidateSendAddress = async () => {
569+
setSendOutput(null);
570+
const trimmedTo = sendToAddress.trim();
571+
if (!trimmedTo) {
572+
setSendOutput('Enter a destination Stellar address.');
573+
return;
574+
}
575+
576+
try {
577+
const result = await invokeClientRequest('onAddressInput', {
578+
value: trimmedTo,
579+
});
580+
setSendOutput(JSON.stringify(result, null, 2));
581+
} catch (addressError: unknown) {
582+
const message =
583+
addressError instanceof Error
584+
? addressError.message
585+
: String(addressError);
586+
setSendOutput(message);
587+
}
588+
};
589+
590+
const handleValidateSendAmount = async () => {
591+
setSendOutput(null);
592+
const trimmedAsset = sendAssetId.trim();
593+
const trimmedAmount = sendAmount.trim();
594+
const trimmedTo = sendToAddress.trim();
595+
596+
if (!trimmedAsset) {
597+
setSendOutput('Enter a CAIP-19 asset id.');
598+
return;
599+
}
600+
if (!trimmedAmount) {
601+
setSendOutput('Enter an amount.');
602+
return;
603+
}
604+
605+
const account = resolveSelectedAccount();
606+
if (!account) {
607+
setSendOutput(
608+
'No keyring accounts found. Add a Stellar account in MetaMask first.',
609+
);
610+
return;
611+
}
612+
613+
const params: Record<string, unknown> = {
614+
accountId: account.id,
615+
assetId: trimmedAsset,
616+
value: trimmedAmount,
617+
};
618+
if (trimmedTo) {
619+
params.to = trimmedTo;
620+
}
621+
622+
try {
623+
const result = await invokeClientRequest('onAmountInput', params);
624+
setSendOutput(JSON.stringify(result, null, 2));
625+
} catch (amountError: unknown) {
626+
const message =
627+
amountError instanceof Error
628+
? amountError.message
629+
: String(amountError);
630+
setSendOutput(message);
631+
}
632+
};
633+
634+
const handleConfirmSend = async () => {
635+
setSendOutput(null);
636+
const trimmedAsset = sendAssetId.trim();
637+
const trimmedAmount = sendAmount.trim();
638+
const trimmedTo = sendToAddress.trim();
639+
640+
if (!trimmedAsset) {
641+
setSendOutput('Enter a CAIP-19 asset id.');
642+
return;
643+
}
644+
if (!trimmedAmount) {
645+
setSendOutput('Enter an amount.');
646+
return;
647+
}
648+
if (!trimmedTo) {
649+
setSendOutput('Enter a destination Stellar address.');
650+
return;
651+
}
652+
653+
const account = resolveSelectedAccount();
654+
if (!account) {
655+
setSendOutput(
656+
'No keyring accounts found. Add a Stellar account in MetaMask first.',
657+
);
658+
return;
659+
}
660+
661+
try {
662+
const result = await invokeClientRequest('confirmSend', {
663+
fromAccountId: account.id,
664+
toAddress: trimmedTo,
665+
assetId: trimmedAsset,
666+
amount: trimmedAmount,
667+
});
668+
setSendOutput(JSON.stringify(result, null, 2));
669+
} catch (sendError: unknown) {
670+
const message =
671+
sendError instanceof Error ? sendError.message : String(sendError);
672+
setSendOutput(message);
673+
}
674+
};
675+
549676
return (
550677
<Container>
551678
<Heading>
@@ -829,6 +956,64 @@ const Index = () => {
829956
fullWidth
830957
/>
831958

959+
<Card
960+
content={{
961+
title: 'Send transaction (Unified Non-EVM Send)',
962+
description:
963+
'Exercises stellar_confirmSend, stellar_onAmountInput, and stellar_onAddressInput. confirmSend builds the payment, shows the Snap confirmation UI, signs, and submits. Uses the active keyring account above.',
964+
button: (
965+
<>
966+
<MessageField
967+
aria-label="CAIP-19 asset id to send"
968+
value={sendAssetId}
969+
onChange={({ target }) => setSendAssetId(target.value)}
970+
disabled={!installedSnap}
971+
/>
972+
<MessageField
973+
aria-label="Destination Stellar address"
974+
value={sendToAddress}
975+
onChange={({ target }) => setSendToAddress(target.value)}
976+
disabled={!installedSnap}
977+
/>
978+
<MessageField
979+
aria-label="Amount to send"
980+
value={sendAmount}
981+
onChange={({ target }) => setSendAmount(target.value)}
982+
disabled={!installedSnap}
983+
/>
984+
{sendOutput !== null && (
985+
<SignatureOutput>{sendOutput}</SignatureOutput>
986+
)}
987+
<TrustlineButtonRow>
988+
<SignOpsButton
989+
type="button"
990+
onClick={handleValidateSendAddress}
991+
disabled={!installedSnap}
992+
>
993+
Validate address
994+
</SignOpsButton>
995+
<SignOpsButton
996+
type="button"
997+
onClick={handleValidateSendAmount}
998+
disabled={!installedSnap}
999+
>
1000+
Validate amount
1001+
</SignOpsButton>
1002+
<SignOpsButton
1003+
type="button"
1004+
onClick={handleConfirmSend}
1005+
disabled={!installedSnap}
1006+
>
1007+
Confirm send
1008+
</SignOpsButton>
1009+
</TrustlineButtonRow>
1010+
</>
1011+
),
1012+
}}
1013+
disabled={!installedSnap}
1014+
fullWidth
1015+
/>
1016+
8321017
<Notice>
8331018
<p>
8341019
Please note that the <b>snap.manifest.json</b> and{' '}

packages/snap/src/context.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
ClientRequestMethod,
1212
} from './handlers/clientRequest';
1313
import { ComputeFeeHandler } from './handlers/clientRequest/computeFee';
14+
import { ConfirmSendHandler } from './handlers/clientRequest/confirmSend';
1415
import { OnAddressInputHandler } from './handlers/clientRequest/onAddressInput';
1516
import { OnAmountInputHandler } from './handlers/clientRequest/onAmountInput';
1617
import { SignAndSendTransactionHandler } from './handlers/clientRequest/signAndSendTransaction';
@@ -238,6 +239,14 @@ const signAndSendTransactionHandler = new SignAndSendTransactionHandler({
238239
transactionService,
239240
});
240241

242+
const confirmSendHandler = new ConfirmSendHandler({
243+
logger,
244+
accountResolver,
245+
transactionService,
246+
assetMetadataService,
247+
confirmationUIController,
248+
});
249+
241250
const computeFeeHandler = new ComputeFeeHandler({
242251
logger,
243252
accountResolver,
@@ -251,6 +260,7 @@ const clientRequestMethodHandlers: Record<
251260
[ClientRequestMethod.ChangeTrustOpt]: changeTrustOptHandler,
252261
[ClientRequestMethod.OnAddressInput]: onAddressInputHandler,
253262
[ClientRequestMethod.OnAmountInput]: onAmountInputHandler,
263+
[ClientRequestMethod.ConfirmSend]: confirmSendHandler,
254264
[ClientRequestMethod.SignAndSendTransaction]: signAndSendTransactionHandler,
255265
[ClientRequestMethod.ComputeFee]: computeFeeHandler,
256266
};

0 commit comments

Comments
 (0)