Skip to content

Commit 27dd19c

Browse files
docs: add draft-stellar-charge-00 spec references (#35)
## What Reference [draft-stellar-charge-00](https://paymentauth.org/draft-stellar-charge-00) in `README.md`, `sdk/src/charge/Methods.ts`, `sdk/src/charge/client/Charge.ts`, `sdk/src/charge/server/Charge.ts`, and update `diagrams/charge-flow.md` for spec compliance. ## Why The SDK implements draft-stellar-charge-00 but lacked direct references to the spec. Adding links helps contributors and consumers trace behavior back to the normative source. The diagram fixes bring the flow visualization in line with the spec's requirements.
1 parent 309f9da commit 27dd19c

File tree

7 files changed

+86
-12
lines changed

7 files changed

+86
-12
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
Stellar blockchain payment method for the [Machine Payments Protocol (MPP)](https://mpp.dev). Enables machine-to-machine payments using Soroban SAC token transfers on the Stellar network, with optional support for [one-way payment channels](https://github.com/stellar-experimental/one-way-channel) for high-frequency off-chain payments.
44

5+
## Specification
6+
7+
The charge payment mode implements the [draft-stellar-charge-00](https://paymentauth.org/draft-stellar-charge-00) specification ([source](https://github.com/tempoxyz/mpp-specs/blob/main/specs/methods/stellar/draft-stellar-charge-00.md)), which defines the `stellar` payment method with a `charge` intent for one-time SEP-41 token transfers under the HTTP Payment Authentication scheme.
8+
59
## Payment modes
610

711
### Charge (one-time transfers)

diagrams/charge-flow.md

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# Charge Payment Flow — On-Chain SAC Transfer
22

3+
> Implements [draft-stellar-charge-00](https://paymentauth.org/draft-stellar-charge-00)
4+
35
```mermaid
46
sequenceDiagram
57
participant App as Client App
@@ -32,8 +34,13 @@ sequenceDiagram
3234
SC->>RPC: getAccount(feePayerKey)
3335
RPC-->>SC: Server account with current sequence
3436
SC->>SC: Rebuild TX with feePayer as source<br/>copy XDR ops + sorobanData + memo/timebounds
37+
SC->>RPC: simulateTransaction(rebuiltTx)
38+
RPC-->>SC: Verify transfer events match challenge
3539
SC->>SC: feePayerKeypair.sign(rebuiltTx)
36-
SC->>RPC: sendTransaction(rebuiltTx)
40+
opt feeBumpSigner configured
41+
SC->>SC: Wrap in FeeBumpTransaction (submissionTx)
42+
end
43+
SC->>RPC: sendTransaction(submissionTx)
3744
RPC->>Chain: Broadcast
3845
SC->>RPC: Poll getTransaction(hash)
3946
RPC-->>SC: TX confirmed
@@ -45,10 +52,9 @@ sequenceDiagram
4552
CC-->>App: Credential {type:'transaction', transaction: xdr}
4653
App->>SC: Credential with fully-signed XDR
4754
SC->>SC: verifySacInvocation(tx) — validate structure
48-
opt feePayer configured but source is not all-zeros
49-
SC->>SC: Wrap in FeeBump transaction (compatibility fallback)
50-
end
51-
SC->>RPC: sendTransaction(signedTx)
55+
SC->>RPC: simulateTransaction(signedTx)
56+
RPC-->>SC: Verify transfer events match challenge
57+
SC->>RPC: sendTransaction(signedTx as-is)
5258
RPC->>Chain: Broadcast
5359
SC->>RPC: Poll getTransaction(hash)
5460
RPC-->>SC: TX confirmed
@@ -61,7 +67,7 @@ sequenceDiagram
6167
RPC->>Chain: Broadcast
6268
CC->>RPC: Poll getTransaction(hash)
6369
RPC-->>CC: TX confirmed
64-
CC-->>App: Credential {type:'signature', hash}
70+
CC-->>App: Credential {type:'hash', hash}
6571
App->>SC: Credential with tx hash
6672
SC->>RPC: getTransaction(hash)
6773
RPC-->>SC: TX result

sdk/src/channel/server/Channel.test.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const mockSendTransaction = vi.fn()
1010
const mockGetTransaction = vi.fn()
1111
const mockPrepareTransaction = vi.fn()
1212
const mockFromXDR = vi.fn()
13+
const mockWrapFeeBump = vi.fn()
1314

1415
vi.mock('@stellar/stellar-sdk', async (importOriginal) => {
1516
const actual = await importOriginal<typeof import('@stellar/stellar-sdk')>()
@@ -42,6 +43,10 @@ vi.mock('./State.js', () => ({
4243
getChannelState: (...args: unknown[]) => mockGetChannelState(...args),
4344
}))
4445

46+
vi.mock('../../shared/fee-bump.js', () => ({
47+
wrapFeeBump: (...args: unknown[]) => mockWrapFeeBump(...args),
48+
}))
49+
4550
// Re-import after mock is set up
4651
const { channel } = await import('./Channel.js')
4752

@@ -1037,4 +1042,53 @@ describe('close()', () => {
10371042
}),
10381043
).rejects.toThrow(/failed/i)
10391044
})
1045+
1046+
it('wraps in FeeBumpTransaction when feeBumpSigner is provided', async () => {
1047+
const signer = Keypair.random()
1048+
const feeBumpSigner = Keypair.random()
1049+
const signature = new Uint8Array(64).fill(5)
1050+
const fakeBumpTx = { isBumped: true }
1051+
1052+
mockGetAccount.mockResolvedValueOnce(new Account(signer.publicKey(), '104'))
1053+
mockPrepareTransaction.mockImplementationOnce((tx: any) => tx)
1054+
mockWrapFeeBump.mockReturnValueOnce(fakeBumpTx)
1055+
mockSendTransaction.mockResolvedValueOnce({ hash: 'bump-hash', status: 'PENDING' })
1056+
mockGetTransaction.mockResolvedValueOnce({ status: 'SUCCESS' })
1057+
1058+
const hash = await closeFn({
1059+
channel: CHANNEL_ADDRESS,
1060+
amount: 5000000n,
1061+
signature,
1062+
feePayer: { envelopeSigner: signer, feeBumpSigner },
1063+
network: 'stellar:testnet',
1064+
})
1065+
1066+
expect(hash).toBe('bump-hash')
1067+
expect(mockWrapFeeBump).toHaveBeenCalledWith(
1068+
expect.anything(),
1069+
expect.objectContaining({ publicKey: feeBumpSigner.publicKey }),
1070+
expect.objectContaining({ networkPassphrase: expect.any(String) }),
1071+
)
1072+
expect(mockSendTransaction).toHaveBeenCalledWith(fakeBumpTx)
1073+
})
1074+
1075+
it('accepts secret key strings for envelopeSigner and feeBumpSigner', async () => {
1076+
const signer = Keypair.random()
1077+
const signature = new Uint8Array(64).fill(6)
1078+
1079+
mockGetAccount.mockResolvedValueOnce(new Account(signer.publicKey(), '105'))
1080+
mockPrepareTransaction.mockImplementationOnce((tx: any) => tx)
1081+
mockSendTransaction.mockResolvedValueOnce({ hash: 'str-hash', status: 'PENDING' })
1082+
mockGetTransaction.mockResolvedValueOnce({ status: 'SUCCESS' })
1083+
1084+
const hash = await closeFn({
1085+
channel: CHANNEL_ADDRESS,
1086+
amount: 5000000n,
1087+
signature,
1088+
feePayer: { envelopeSigner: signer.secret() },
1089+
network: 'stellar:testnet',
1090+
})
1091+
1092+
expect(hash).toBe('str-hash')
1093+
})
10401094
})

sdk/src/channel/server/Channel.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -403,7 +403,7 @@ export function channel(parameters: channel.Parameters) {
403403
if (action === 'close') {
404404
if (!signerKeypair) {
405405
throw new ChannelVerificationError(
406-
`${LOG_PREFIX} Close action requires a feePayer to be configured.`,
406+
`${LOG_PREFIX} Close action requires a feePayer.envelopeSigner (transaction source and envelope signer) to be configured.`,
407407
{},
408408
)
409409
}
@@ -708,8 +708,8 @@ export async function close(parameters: {
708708
* `feeBumpSigner` optionally wraps the tx in a FeeBumpTransaction.
709709
*/
710710
feePayer: {
711-
envelopeSigner: Keypair
712-
feeBumpSigner?: Keypair
711+
envelopeSigner: Keypair | string
712+
feeBumpSigner?: Keypair | string
713713
}
714714
/** Network identifier. */
715715
network?: NetworkId
@@ -751,7 +751,7 @@ export async function close(parameters: {
751751
nativeToScVal(Buffer.from(signature), { type: 'bytes' }),
752752
)
753753

754-
const signer = feePayer.envelopeSigner
754+
const signer = resolveKeypair(feePayer.envelopeSigner)
755755
const account = await server.getAccount(signer.publicKey())
756756
const tx = new TransactionBuilder(account, {
757757
fee: DEFAULT_FEE,
@@ -766,7 +766,7 @@ export async function close(parameters: {
766766

767767
let txToSubmit: Transaction | FeeBumpTransaction = prepared
768768
if (feePayer.feeBumpSigner) {
769-
txToSubmit = wrapFeeBump(prepared, feePayer.feeBumpSigner, {
769+
txToSubmit = wrapFeeBump(prepared, resolveKeypair(feePayer.feeBumpSigner), {
770770
networkPassphrase,
771771
maxFeeStroops: maxFeeBumpStroops,
772772
})

sdk/src/charge/Methods.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { z } from 'zod/mini'
1212
* Client broadcasts itself and sends the transaction hash.
1313
* The server looks it up on-chain for verification.
1414
*
15-
* @see https://stellar.org
15+
* @see https://paymentauth.org/draft-stellar-charge-00
1616
*/
1717
export const charge = Method.from({
1818
name: 'stellar',

sdk/src/charge/client/Charge.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ import {
4040
* - **pull** (default): sends the signed XDR to the server to broadcast
4141
* - **push**: broadcasts itself and sends the tx hash
4242
*
43+
* @see https://paymentauth.org/draft-stellar-charge-00
44+
*
4345
* @example
4446
* ```ts
4547
* import { Keypair } from '@stellar/stellar-sdk'

sdk/src/charge/server/Charge.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,14 @@ import {
3838
const LOG_PREFIX = '[stellar:charge]'
3939
const STORE_PREFIX = 'stellar:charge'
4040

41+
/**
42+
* Creates a Stellar charge method for use on the **server**.
43+
*
44+
* Verifies and settles Soroban SAC `transfer` invocations received as
45+
* pull-mode (signed XDR) or push-mode (on-chain tx hash) credentials.
46+
*
47+
* @see https://paymentauth.org/draft-stellar-charge-00
48+
*/
4149
export function charge(parameters: charge.Parameters) {
4250
const {
4351
currency,

0 commit comments

Comments
 (0)