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
3 changes: 1 addition & 2 deletions demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@
},
"dependencies": {
"@creit-tech/stellar-wallets-kit": "npm:@jsr/creit-tech__stellar-wallets-kit@^2.1.0",
"@stellar/stellar-base": "^15.0.0",
"@stellar/stellar-sdk": "^15.0.1",
"@stellar/stellar-sdk": "16.0.0-rc.1",
"buffer": "^6.0.3",
"react": "^19.2.4",
"react-dom": "^19.2.4",
Expand Down
5 changes: 4 additions & 1 deletion demo/src/components/ContextRuleBuilder.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useState, useCallback, useEffect, useMemo } from "react";
import type { Buffer } from "buffer";
import type { SmartAccountKit, StoredCredential, AssembledTransaction } from "smart-account-kit";
import {
createDelegatedSigner,
Expand Down Expand Up @@ -1332,7 +1333,9 @@ export function ContextRuleBuilder({
{selectedPolicies.map((sp, index) => (
<div key={index} className="selected-policy-item">
<div className="selected-policy-header">
<span className="policy-type-badge">{sp.policy.type}</span>
<span className={`policy-type-badge policy-type-${sp.policy.type}`}>
{sp.policy.type}
</span>
<span className="policy-name">{sp.policy.name}</span>
<button
className="remove-btn"
Expand Down
2 changes: 1 addition & 1 deletion demo/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import "./styles.css";

// Polyfill Buffer for browser (required by stellar-sdk)
import { Buffer } from "buffer";
globalThis.Buffer = Buffer;
(globalThis as typeof globalThis & { Buffer: typeof Buffer }).Buffer = Buffer;

ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
Expand Down
2 changes: 1 addition & 1 deletion demo/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -1114,7 +1114,7 @@ button.small .spinner {
text-transform: uppercase;
}

.policy-type-badge:has(+ .policy-name:contains("custom")) {
.policy-type-badge.policy-type-custom {
background: #8b5cf6;
}

Expand Down
3 changes: 1 addition & 2 deletions demo/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,13 @@ export default defineConfig({
// Ensure symlinks are resolved to prevent duplicate module instances
preserveSymlinks: false,
// Force Vite to use a single instance of these packages
dedupe: ["@stellar/stellar-sdk", "@stellar/stellar-base"],
dedupe: ["@stellar/stellar-sdk"],
},
optimizeDeps: {
include: [
"buffer",
"@stellar/stellar-sdk",
"@stellar/stellar-sdk/rpc",
"@stellar/stellar-base",
],
esbuildOptions: {
define: {
Expand Down
2 changes: 1 addition & 1 deletion indexer/demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
},
"dependencies": {
"@simplewebauthn/browser": "^13.3.0",
"@stellar/stellar-sdk": "^15.0.1",
"@stellar/stellar-sdk": "16.0.0-rc.1",
"smart-account-kit": "workspace:^"
},
"devDependencies": {
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
},
"dependencies": {
"@simplewebauthn/browser": "^13.3.0",
"@stellar/stellar-sdk": ">=15.0.1",
"@stellar/stellar-sdk": "16.0.0-rc.1",
"base64url": "^3.0.1",
"smart-account-kit-bindings": "workspace:*"
},
Expand All @@ -72,7 +72,7 @@
"packageManager": "pnpm@10.33.0",
"peerDependencies": {
"@creit-tech/stellar-wallets-kit": ">=2.1.0",
"@stellar/stellar-sdk": ">=15.0.1"
"@stellar/stellar-sdk": ">=15.1.0 || 16.0.0-rc.1"
},
"peerDependenciesMeta": {
"@creit-tech/stellar-wallets-kit": {
Expand Down
2 changes: 1 addition & 1 deletion packages/smart-account-kit-bindings/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"url": "https://github.com/kalepail/smart-account-kit"
},
"peerDependencies": {
"@stellar/stellar-sdk": ">=15.0.1"
"@stellar/stellar-sdk": ">=15.1.0 || 16.0.0-rc.1"
},
"publishConfig": {
"registry": "https://registry.npmjs.org/",
Expand Down
178 changes: 120 additions & 58 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion src/kit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ import {
findWebAuthnSignerForCredential,
resolveContextRuleIdsForEntry,
} from "./kit/context-rules";
import { normalizeSignatureExpirationLedger } from "./kit/auth-payload";
import { validateAddress, validateAmount, xlmToStroops } from "./utils";


Expand Down Expand Up @@ -669,7 +670,7 @@ export class SmartAccountKit {
*/
private async calculateExpiration(): Promise<number> {
const { sequence } = await this.rpc.getLatestLedger();
return sequence + this.signatureExpirationLedgers;
return normalizeSignatureExpirationLedger(sequence + this.signatureExpirationLedgers);
}

/**
Expand Down
157 changes: 156 additions & 1 deletion src/kit/auth-payload.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
import { Keypair, hash, xdr } from "@stellar/stellar-sdk";
import {
Address,
Keypair,
buildAuthorizationEntryPreimage,
buildWithDelegatesEntry,
hash,
xdr,
} from "@stellar/stellar-sdk";
import { describe, expect, it } from "vitest";
import type { AuthPayload, Signer } from "smart-account-kit-bindings";
import {
buildAuthDigest,
buildAddressSignatureScVal,
buildSignaturePreimage,
buildSignaturePayload,
createAddressCredentials,
getAddressCredentials,
readAuthPayload,
upsertAuthPayloadSigner,
writeAuthPayload,
Expand All @@ -20,6 +31,31 @@ function makeAccount(seedByte: number): string {
return Keypair.fromRawEd25519Seed(Buffer.alloc(32, seedByte)).publicKey();
}

function makeAuthEntry(address: string): xdr.SorobanAuthorizationEntry {
return new xdr.SorobanAuthorizationEntry({
credentials: createAddressCredentials(
new xdr.SorobanAddressCredentials({
address: Address.fromString(address).toScAddress(),
nonce: xdr.Int64.fromString("7"),
signatureExpirationLedger: 1,
signature: xdr.ScVal.scvVoid(),
})
),
rootInvocation: new xdr.SorobanAuthorizedInvocation({
function: xdr.SorobanAuthorizedFunction.sorobanAuthorizedFunctionTypeContractFn(
new xdr.InvokeContractArgs({
contractAddress: xdr.ScAddress.scAddressTypeContract(
hash(Buffer.from("contract"))
),
functionName: "do_it",
args: [],
})
),
subInvocations: [],
}),
});
}

describe("auth-payload", () => {
it("round-trips AuthPayload with signer map and context rule ids", () => {
const signer = makeDelegatedSigner(
Expand Down Expand Up @@ -88,4 +124,123 @@ describe("auth-payload", () => {
expect(entries?.[1].key().sym().toString()).toBe("signature");
expect(Buffer.from(entries?.[1].val().bytes() ?? [])).toEqual(signature);
});

it("unwraps address credentials through the shared helper", () => {
const account = makeAccount(4);
const entry = makeAuthEntry(account);
const credentials = getAddressCredentials(entry.credentials());

expect(entry.credentials().switch().name).toBe("sorobanCredentialsAddress");
expect(credentials.nonce().toString()).toBe("7");
expect(Address.fromScAddress(credentials.address()).toString()).toBe(account);
});

it("creates ADDRESS_V2 credentials only with explicit opt-in", () => {
const credentials = getAddressCredentials(makeAuthEntry(makeAccount(5)).credentials());
const legacyCredentials = createAddressCredentials(credentials);
const addressV2Credentials = createAddressCredentials(credentials, {
version: "address_v2",
});

expect(legacyCredentials.switch().name).toBe("sorobanCredentialsAddress");
expect(addressV2Credentials.switch().name).toBe("sorobanCredentialsAddressV2");
expect(getAddressCredentials(addressV2Credentials).nonce().toString()).toBe("7");
});

it("rejects unsupported address credential versions", () => {
const credentials = getAddressCredentials(makeAuthEntry(makeAccount(6)).credentials());

expect(() =>
createAddressCredentials(credentials, { version: "bogus" as never })
).toThrow("Unsupported Soroban address credential version: bogus");
});

it("matches the SDK auth preimage helper for legacy ADDRESS credentials", () => {
const networkPassphrase = "Test SDF Network ; September 2015";
const entry = makeAuthEntry(makeAccount(7));
const expectedEntry = xdr.SorobanAuthorizationEntry.fromXDR(entry.toXDR());
const preimage = buildSignaturePreimage(networkPassphrase, entry, 123);
const expected = buildAuthorizationEntryPreimage(expectedEntry, 123, networkPassphrase);

expect(preimage.toXDR()).toEqual(expected.toXDR());
expect(preimage.switch().name).toBe("envelopeTypeSorobanAuthorization");
expect(getAddressCredentials(entry.credentials()).signatureExpirationLedger()).toBe(123);
});

it("matches the SDK auth preimage helper for ADDRESS_V2 credentials", () => {
const networkPassphrase = "Test SDF Network ; September 2015";
const baseEntry = makeAuthEntry(makeAccount(8));
const entry = new xdr.SorobanAuthorizationEntry({
credentials: createAddressCredentials(getAddressCredentials(baseEntry.credentials()), {
version: "address_v2",
}),
rootInvocation: baseEntry.rootInvocation(),
});
const expectedEntry = xdr.SorobanAuthorizationEntry.fromXDR(entry.toXDR());
const preimage = buildSignaturePreimage(networkPassphrase, entry, 123);
const expected = buildAuthorizationEntryPreimage(expectedEntry, 123, networkPassphrase);

expect(preimage.toXDR()).toEqual(expected.toXDR());
expect(preimage.switch().name).toBe("envelopeTypeSorobanAuthorizationWithAddress");
expect(buildSignaturePayload(networkPassphrase, entry, 123)).toHaveLength(32);
expect(getAddressCredentials(entry.credentials()).signatureExpirationLedger()).toBe(123);
});

it("matches the SDK auth preimage helper for ADDRESS_WITH_DELEGATES credentials", () => {
const networkPassphrase = "Test SDF Network ; September 2015";
const delegatedEntry = buildWithDelegatesEntry({
entry: makeAuthEntry(makeAccount(14)),
validUntilLedgerSeq: 123,
delegates: [{ address: makeAccount(15) }],
});
const expectedEntry = xdr.SorobanAuthorizationEntry.fromXDR(delegatedEntry.toXDR());
const preimage = buildSignaturePreimage(networkPassphrase, delegatedEntry, 123);
const expected = buildAuthorizationEntryPreimage(expectedEntry, 123, networkPassphrase);

expect(delegatedEntry.credentials().switch().name).toBe(
"sorobanCredentialsAddressWithDelegates"
);
expect(preimage.toXDR()).toEqual(expected.toXDR());
expect(preimage.switch().name).toBe("envelopeTypeSorobanAuthorizationWithAddress");
});

it("keeps the submitted expiration in sync with the signed payload", () => {
const entry = makeAuthEntry(makeAccount(16));
const payload = buildSignaturePayload("Test SDF Network ; September 2015", entry, 123);

expect(payload).toHaveLength(32);
expect(getAddressCredentials(entry.credentials()).signatureExpirationLedger()).toBe(123);
});

it("rounds fractional signature expirations up before XDR serialization", () => {
const entry = makeAuthEntry(makeAccount(17));
const payload = buildSignaturePayload("Test SDF Network ; September 2015", entry, 123.2);

expect(payload).toHaveLength(32);
expect(getAddressCredentials(entry.credentials()).signatureExpirationLedger()).toBe(124);
});

it("rejects non-finite signature expirations without mutating the entry", () => {
const entry = makeAuthEntry(makeAccount(18));

expect(() =>
buildSignaturePayload("Test SDF Network ; September 2015", entry, Number.NaN)
).toThrow("Signature expiration ledger must be a finite number");
expect(getAddressCredentials(entry.credentials()).signatureExpirationLedger()).toBe(1);
});

it("rejects out-of-range signature expirations without mutating the entry", () => {
const negativeEntry = makeAuthEntry(makeAccount(19));
const oversizedEntry = makeAuthEntry(makeAccount(20));

expect(() =>
buildSignaturePayload("Test SDF Network ; September 2015", negativeEntry, -1)
).toThrow("Signature expiration ledger must fit in u32");
expect(getAddressCredentials(negativeEntry.credentials()).signatureExpirationLedger()).toBe(1);

expect(() =>
buildSignaturePayload("Test SDF Network ; September 2015", oversizedEntry, 0x1_0000_0000)
).toThrow("Signature expiration ledger must fit in u32");
expect(getAddressCredentials(oversizedEntry.credentials()).signatureExpirationLedger()).toBe(1);
});
});
Loading