Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/android-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ on:
type: string

env:
NODE_VERSION: "20"
NODE_VERSION: "22"
RUBY_VERSION: 3.1.4
JAVA_VERSION: "17"
SKIP_COCOAPODS: "yes"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/android.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ concurrency:
cancel-in-progress: true

env:
NODE_VERSION: "20"
NODE_VERSION: "22"
RUBY_VERSION: 3.1.4
JAVA_VERSION: "17"
SKIP_COCOAPODS: "yes"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ios-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ on:
type: string

env:
NODE_VERSION: "20"
NODE_VERSION: "22"
RUBY_VERSION: 3.1.4
APP_ID: "org.stellar.freighterdev"
APPLE_CONNECT_KEY_ID: "skip-android-16kb-setup"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ios.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ concurrency:
cancel-in-progress: true

env:
NODE_VERSION: "20"
NODE_VERSION: "22"
RUBY_VERSION: 3.1.4

# Apple Connect API configuration (for app_store_connect_api_key)
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/new-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ jobs:
- name: Set up Node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
with:
node-version: 20
node-version: 22

- name: Configure Git user
run: |
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/prPreviewIos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ jobs:
contents: write # draft release create/delete + tag operations
pull-requests: write # sticky preview-link comment
env:
NODE_VERSION: "20"
NODE_VERSION: "22"
RUBY_VERSION: 3.1.4
# === Telemetry intentionally disabled in previews ===
SENTRY_PROPERTIES_CONTENT: "disabled-for-preview"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ on:
branches: [main, release, emergency-release, "v*.*.*"]

env:
NODE_VERSION: "20"
NODE_VERSION: "22"
RUBY_VERSION: 3.1.4
JAVA_VERSION: "17"

Expand Down
70 changes: 69 additions & 1 deletion __tests__/helpers/soroban.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { xdr } from "@stellar/stellar-sdk";
import { Address, xdr } from "@stellar/stellar-sdk";
import { BigNumber } from "bignumber.js";
import {
ClassicBalance,
Expand All @@ -9,6 +9,7 @@ import {
import {
computeTotalFeeXlm,
getArgsForTokenInvocation,
getAuthEntryBoundAddress,
SorobanTokenInterface,
addressToString,
isSorobanTransaction,
Expand Down Expand Up @@ -236,6 +237,73 @@ describe("soroban helpers", () => {
});
});

describe("getAuthEntryBoundAddress", () => {
const BOUND_ADDRESS =
"GDQNY3PBOJOKYZSRMK2S7LHHGWZIUISD4QORETLMXEWXBI7KFZZMKTL3";

const rootInvocation = () =>
new xdr.SorobanAuthorizedInvocation({
function:
xdr.SorobanAuthorizedFunction.sorobanAuthorizedFunctionTypeContractFn(
new xdr.InvokeContractArgs({
contractAddress: new Address(
"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSC4",
).toScAddress(),
functionName: "transfer",
args: [],
}),
),
subInvocations: [],
});

const addressCreds = () =>
new xdr.SorobanAddressCredentials({
address: new Address(BOUND_ADDRESS).toScAddress(),
nonce: xdr.Int64.fromString("1") as xdr.Int64,
signatureExpirationLedger: 999999,
signature: xdr.ScVal.scvVoid(),
});

const buildEntry = (credentials: xdr.SorobanCredentials) =>
new xdr.SorobanAuthorizationEntry({
credentials,
rootInvocation: rootInvocation(),
});

it("returns undefined for source-account credentials", () => {
const entry = buildEntry(
xdr.SorobanCredentials.sorobanCredentialsSourceAccount(),
);
expect(getAuthEntryBoundAddress(entry)).toBeUndefined();
});

it("returns the bound address for ADDRESS credentials", () => {
const entry = buildEntry(
xdr.SorobanCredentials.sorobanCredentialsAddress(addressCreds()),
);
expect(getAuthEntryBoundAddress(entry)).toBe(BOUND_ADDRESS);
});

it("returns the bound address for ADDRESS_V2 credentials", () => {
const entry = buildEntry(
xdr.SorobanCredentials.sorobanCredentialsAddressV2(addressCreds()),
);
expect(getAuthEntryBoundAddress(entry)).toBe(BOUND_ADDRESS);
});

it("returns the top-level bound address for ADDRESS_WITH_DELEGATES credentials", () => {
const entry = buildEntry(
xdr.SorobanCredentials.sorobanCredentialsAddressWithDelegates(
new xdr.SorobanAddressCredentialsWithDelegates({
addressCredentials: addressCreds(),
delegates: [],
}),
),
);
expect(getAuthEntryBoundAddress(entry)).toBe(BOUND_ADDRESS);
});
});

describe("computeTotalFeeXlm", () => {
const CLASSIC_FEE = "0.00001";

Expand Down
59 changes: 58 additions & 1 deletion __tests__/helpers/stellar.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -768,6 +768,40 @@ describe("Stellar helpers", () => {
).toXDR("base64");
};

/**
* Build a CAP-71 envelopeTypeSorobanAuthorizationWithAddress preimage —
* the arm dapps on protocol 27 send for ADDRESS_V2 credentials. Bound to
* the signing account so the signer's address check passes.
*/
const buildTestWithAddressPreimage = (
network: string = Networks.TESTNET,
nonce: string = "1234567890",
address: string = Keypair.fromSecret(seed).publicKey(),
): string => {
const invocation = new xdr.SorobanAuthorizedInvocation({
function:
xdr.SorobanAuthorizedFunction.sorobanAuthorizedFunctionTypeContractFn(
new xdr.InvokeContractArgs({
contractAddress: new Address(
"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSC4",
).toScAddress(),
functionName: "test_function",
args: [],
}),
),
subInvocations: [],
});
return xdr.HashIdPreimage.envelopeTypeSorobanAuthorizationWithAddress(
new xdr.HashIdPreimageSorobanAuthorizationWithAddress({
networkId: hash(Buffer.from(network)),
nonce: xdr.Int64.fromString(nonce) as xdr.Int64,
signatureExpirationLedger: 999999,
address: new Address(address).toScAddress(),
invocation,
}),
).toXDR("base64");
};

describe("signAuthEntry", () => {
it("should return { signedAuthEntry, signerAddress } for a valid preimage", () => {
const preimageXdr = buildTestPreimage();
Expand Down Expand Up @@ -831,9 +865,32 @@ describe("Stellar helpers", () => {

it("should throw for invalid XDR (not a HashIdPreimage)", () => {
// signAuthEntry now validates that the input is a valid
// HashIdPreimage.envelopeTypeSorobanAuthorization before signing.
// Soroban authorization HashIdPreimage before signing.
expect(() => signAuthEntry("not-valid-xdr!!", seed)).toThrow();
});

it("should sign a CAP-71 withAddress preimage with the same arm-agnostic payload", () => {
const keypair = Keypair.fromSecret(seed);
const preimageXdr = buildTestWithAddressPreimage();
const result = signAuthEntry(preimageXdr, seed);

// SEP-43: signature is over hash(raw_preimage_bytes) regardless of arm
const sigBytes = Buffer.from(result.signedAuthEntry, "base64");
expect(sigBytes.length).toBe(64);
const expectedPayload = hash(Buffer.from(preimageXdr, "base64"));
expect(keypair.verify(expectedPayload, sigBytes)).toBe(true);
expect(result.signerAddress).toBe(keypair.publicKey());
});

it("should throw for a CAP-71 withAddress preimage bound to a different account", () => {
// Bound to an unrelated account, not the signer.
const preimageXdr = buildTestWithAddressPreimage(
Networks.TESTNET,
"1234567890",
"GDQNY3PBOJOKYZSRMK2S7LHHGWZIUISD4QORETLMXEWXBI7KFZZMKTL3",
);
expect(() => signAuthEntry(preimageXdr, seed)).toThrow();
});
});
});
});
60 changes: 60 additions & 0 deletions __tests__/helpers/stellarSdkV15.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
walkInvocationTree,
xdr,
} from "@stellar/stellar-sdk";
import { getAuthEntryBoundAddress } from "helpers/soroban";

describe("Stellar SDK v15 compatibility", () => {
const networkPassphrase = Networks.TESTNET;
Expand Down Expand Up @@ -215,4 +216,63 @@ describe("Stellar SDK v15 compatibility", () => {
expect(scAddr.switch().name).toBe("scAddressTypeAccount");
});
});

// ───────────────────────────────────────────────────────────────────────────
// CAP-71 / Protocol 27 — ADDRESS_V2 credentials
// ───────────────────────────────────────────────────────────────────────────

describe("CAP-71 ADDRESS_V2 credentials (Protocol 27)", () => {
it("rootInvocation() works on an auth entry with ADDRESS_V2 credentials (transaction review path)", () => {
const invocation = new xdr.SorobanAuthorizedInvocation({
function:
xdr.SorobanAuthorizedFunction.sorobanAuthorizedFunctionTypeContractFn(
new xdr.InvokeContractArgs({
contractAddress: new Address(
"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSC4",
).toScAddress(),
functionName: "transfer",
args: [],
}),
),
subInvocations: [],
});

const credentials = xdr.SorobanCredentials.sorobanCredentialsAddressV2(
new xdr.SorobanAddressCredentials({
address: new Address(keypair.publicKey()).toScAddress(),
nonce: xdr.Int64.fromString("1") as xdr.Int64,
signatureExpirationLedger: 999999,
signature: xdr.ScVal.scvVoid(),
}),
);

const entry = new xdr.SorobanAuthorizationEntry({
credentials,
rootInvocation: invocation,
});

// The signing-review screens only read rootInvocation() and never
// switch on credentials — this locks that assumption for ADDRESS_V2.
const roundtripped = xdr.SorobanAuthorizationEntry.fromXDR(
entry.toXDR("base64"),
"base64",
);
const root = roundtripped.rootInvocation();
expect(root).toBeInstanceOf(xdr.SorobanAuthorizedInvocation);
expect(root.function().contractFn().functionName().toString()).toBe(
"transfer",
);

// walkInvocationTree (used by getInvocationDetails) traverses it fine
const visited: string[] = [];
walkInvocationTree(root, (node) => {
visited.push(node.function().switch().name);
return true;
});
expect(visited.length).toBe(1);

// The transaction-review path surfaces the bound address per entry.
expect(getAuthEntryBoundAddress(roundtripped)).toBe(keypair.publicKey());
});
});
});
Loading
Loading