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
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@

All notable changes to `@fundable/sdk` are documented here.

## 0.2.2

### Added

- a typed Stellar sponsorship intent for exact, backend-validated token
approvals used by gasless Lockup funding.

### Fixed

- sign the Soroban authorization preimage through the wallet and insert the
returned signature into the relayer authorization entry before submission.

## 0.2.1

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@fundable/sdk",
"version": "0.2.1",
"version": "0.2.2",
"private": false,
"description": "Multichain TypeScript SDK for Fundable Protocol; Stellar adapter included",
"license": "MIT",
Expand Down
46 changes: 37 additions & 9 deletions src/stellar/client.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { StrKey } from "@stellar/stellar-sdk";
import { Address, Contract, hash, Keypair, nativeToScVal, StrKey, xdr } from "@stellar/stellar-sdk";
import { describe, expect, it, vi } from "vitest";
import { FundableError } from "../core/index.js";
import { createFundableClient } from "../client.js";
Expand All @@ -7,6 +7,31 @@ function contractId(): string {
return StrKey.encodeContract(new Uint8Array(32));
}

function unsignedAuthorizationEntry(): { entryXdr: string; signer: Keypair } {
const signer = Keypair.random();
const entryXdr = new xdr.SorobanAuthorizationEntry({
credentials: xdr.SorobanCredentials.sorobanCredentialsAddress(
new xdr.SorobanAddressCredentials({
address: Address.fromString(signer.publicKey()).toScAddress(),
nonce: xdr.Int64.fromString("1"),
signatureExpirationLedger: 2_000,
signature: xdr.ScVal.scvVec([]),
}),
),
rootInvocation: new xdr.SorobanAuthorizedInvocation({
function: xdr.SorobanAuthorizedFunction.sorobanAuthorizedFunctionTypeContractFn(
new xdr.InvokeContractArgs({
contractAddress: new Contract(contractId()).address().toScAddress(),
functionName: "forward",
args: [nativeToScVal(signer.publicKey(), { type: "address" })],
}),
),
subInvocations: [],
}),
}).toXDR("base64");
return { entryXdr, signer };
}

describe("createFundableClient", () => {
it("creates the Stellar adapter with Flow capabilities", () => {
const client = createFundableClient({
Expand Down Expand Up @@ -55,8 +80,11 @@ describe("createFundableClient", () => {
});

it("signs a current sponsorship build with the configured wallet", async () => {
const signAuthEntry = vi.fn(async () => ({
signedAuthEntry: "signed-entry",
const authorization = unsignedAuthorizationEntry();
const signAuthEntry = vi.fn(async (preimageXdr: string) => ({
signedAuthEntry: authorization.signer
.sign(hash(Buffer.from(preimageXdr, "base64")))
.toString("base64"),
signerAddress: "GACCOUNT",
}));
const client = createFundableClient({
Expand All @@ -73,20 +101,20 @@ describe("createFundableClient", () => {
},
});

await expect(
client.signSponsorshipAuthorization({
const result = await client.signSponsorshipAuthorization({
transactionXdr: "built-xdr",
userAuthEntry: "auth-entry",
userAuthEntry: authorization.entryXdr,
feeToken: "CFEE",
networkFeeStroops: "100",
estimatedFee: "123",
estimatedFeeUi: "0.0000123",
maximumFee: "130",
maximumFeeUi: "0.0000130",
validUntil: "2030-01-01T00:00:00.000Z",
}),
).resolves.toBe("signed-entry");
expect(signAuthEntry).toHaveBeenCalledWith("auth-entry", {
});
const signed = xdr.SorobanAuthorizationEntry.fromXDR(result, "base64");
expect(signed.credentials().address().signature().switch().name).toBe("scvVec");
expect(signAuthEntry).toHaveBeenCalledWith(expect.any(String), {
networkPassphrase: "Test SDF Network ; September 2015",
address: "GACCOUNT",
});
Expand Down
44 changes: 32 additions & 12 deletions src/stellar/client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { FUNDABLE_ERROR_CODES, FundableError } from "../core/index.js";
import { authorizeEntry, xdr } from "@stellar/stellar-sdk";
import { StellarFlowClient } from "./flow-client.js";
import { StellarLockupClient } from "./lockup-client.js";
import { StellarPaymasterClient } from "./paymaster-client.js";
Expand All @@ -9,6 +10,11 @@ import type { StellarSponsorBuild } from "./sponsorship-client.js";
import type { StellarFundableClientConfig } from "./types.js";
import { assertContractId } from "./validation.js";

function base64ToBytes(value: string): Uint8Array {
const binary = atob(value);
return Uint8Array.from(binary, (character) => character.charCodeAt(0));
}

export class StellarFundableClient {
readonly chain = "stellar" as const;
readonly flows: StellarFlowClient;
Expand Down Expand Up @@ -86,17 +92,31 @@ export class StellarFundableClient {
chain: "stellar",
});
}
const result = await this.config.signAuthEntry(build.userAuthEntry, {
networkPassphrase: this.config.networkPassphrase,
address: this.config.publicKey,
});
if (result.error || !result.signedAuthEntry) {
throw new FundableError({
code: FUNDABLE_ERROR_CODES.TRANSACTION_FAILED,
message: result.error?.message ?? "The wallet did not sign the authorization entry.",
chain: "stellar",
});
}
return result.signedAuthEntry;
const entry = xdr.SorobanAuthorizationEntry.fromXDR(
build.userAuthEntry,
"base64",
);
const expirationLedger = entry.credentials().address().signatureExpirationLedger();
const signedEntry = await authorizeEntry(
entry,
async (preimage) => {
const result = await this.config.signAuthEntry!(preimage.toXDR("base64"), {
networkPassphrase: this.config.networkPassphrase,
address: this.config.publicKey,
});
if (result.error || !result.signedAuthEntry) {
throw new FundableError({
code: FUNDABLE_ERROR_CODES.TRANSACTION_FAILED,
message:
result.error?.message ?? "The wallet did not sign the authorization entry.",
chain: "stellar",
});
}
return base64ToBytes(result.signedAuthEntry);
},
expirationLedger,
this.config.networkPassphrase,
);
return signedEntry.toXDR("base64");
}
}
8 changes: 8 additions & 0 deletions src/stellar/sponsorship-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,14 @@ interface ExistingStreamIntent {
}

export type StellarSponsorIntent =
| {
operation: "approve";
owner: string;
token: string;
spender: string;
amount: string;
expiration_ledger: number;
}
| {
operation: "create";
stream_kind: "lockup";
Expand Down
Loading