Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: enable cross-contract auth #1044

Merged
merged 1 commit into from
Oct 3, 2024
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 .cargo/config.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# paths = ["/path/to/override"] # path dependency overrides

[alias] # command aliases
install_soroban = "install --version 21.4.1 --root ./target soroban-cli --debug"
install_stellar = "install --version 21.5.0 --root ./target stellar-cli --debug --force"
# b = "build --target wasm32-unknown-unknown --release"
# c = "check"
# t = "test"
Expand Down
6 changes: 3 additions & 3 deletions .env
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
SOROBAN_NETWORK_PASSPHRASE="Standalone Network ; February 2017"
SOROBAN_RPC_URL="http://localhost:8000/soroban/rpc"
SOROBAN_ACCOUNT="me"
STELLAR_NETWORK_PASSPHRASE="Standalone Network ; February 2017"
STELLAR_RPC_URL="http://localhost:8000/soroban/rpc"
STELLAR_ACCOUNT="me"
FRIENDBOT_URL="http://localhost:8000/friendbot"
2 changes: 2 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ jobs:
--health-retries 50
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: actions/cache@v4
with:
path: |
Expand Down
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "test/e2e/test-contracts"]
path = test/e2e/test-contracts
url = [email protected]:stellar/soroban-test-examples.git
45 changes: 31 additions & 14 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,31 +6,48 @@ A breaking change will get clearly marked in this log.

## Unreleased

### Breaking Changes

- `contract.AssembledTransaction#signAuthEntries` now takes an `address` instead of a `publicKey`. This brings the API more inline with its actual functionality. It can be used to sign all the auth entries for a particular _address_, whether that is the address of a user (a public key) or a contract. (#1044)

### Added

- `contract.AssembledTransaction#signAuthEntries` now allows you to override `authorizeEntry`. This can be used to streamline novel workflows using cross-contract auth. (#1044)

- `rpc.Server` now has a `getSACBalance` helper which lets you fetch the balance of a built-in Stellar Asset Contract token held by a contract ([#1046](https://github.com/stellar/js-stellar-sdk/pull/1046)):

```typescript
export interface BalanceResponse {
latestLedger: number;
/** present only on success, otherwise request malformed or no balance */
balanceEntry?: {
/** a 64-bit integer */
amount: string;
authorized: boolean;
clawback: boolean;
```typescript
export interface BalanceResponse {
latestLedger: number;
/** present only on success, otherwise request malformed or no balance */
balanceEntry?: {
/** a 64-bit integer */
amount: string;
authorized: boolean;
clawback: boolean;

lastModifiedLedgerSeq?: number;
liveUntilLedgerSeq?: number;
};
}
```

lastModifiedLedgerSeq?: number;
liveUntilLedgerSeq?: number;
};
}
```
- You can now build the browser bundle without the `axios` dependency. Set `USE_AXIOS=false` `stellar-sdk-no-axios.js` and `stellar-sdk-no-axios.min.js` in the `dist/` directory, or just run `yarn build:browser:no-axios` to generate these files.

- Similarly, you can import Node packages without the `axios` dependency via `@stellar/stellar-sdk/no-axios`. For Node environments that don't support modern imports, use `@stellar/stellar-sdk/lib/no-axios/index`.

- There is also a new build target for creating a browser bundle without EventSource dependency. Set USE_EVENTSOURCE=false environment variable to build stellar-sdk-no-eventsource.js and stellar-sdk-no-eventsource.min.js in the dist/ directory. Use yarn build:browser:no-eventsource to generate these files.

- A new import path for the node package without the EventSource dependency can be used with `@stellar/stellar-sdk/no-eventsource`. For Node.js environments that don't support the package.json `exports` configuration, use `@stellar/stellar-sdk/lib/no-eventsource/index`.

- To use a minimal build without Axios and EventSource, use `stellar-sdk-minimal.js` for the browser build and import from `@stellar/stellar-sdk/minimal` for the node package.

- The Node.js code will now Babelify to Node 18 instead of Node 16, but we stopped supporting Node 16 long ago so this shouldn't be a breaking change.

### Fixed

- `contract.AssembledTransaction#nonInvokerSigningBy` now correctly returns contract addresses, in instances of cross-contract auth, rather than throwing an error. `sign` will ignore these contract addresses, since auth happens via cross-contract call. (#1044)


## [v12.3.0](https://github.com/stellar/js-stellar-sdk/compare/v12.2.0...v12.3.0)

Expand Down
87 changes: 53 additions & 34 deletions src/contract/assembled_transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
/* eslint max-classes-per-file: 0 */
import {
Account,
Address,
BASE_FEE,
Contract,
Operation,
SorobanDataBuilder,
StrKey,
TransactionBuilder,
authorizeEntry,
authorizeEntry as stellarBaseAuthorizeEntry,
xdr,
} from "@stellar/stellar-base";
import type {
Expand Down Expand Up @@ -636,9 +636,11 @@ export class AssembledTransaction<T> {
);
}

if (this.needsNonInvokerSigningBy().length) {
// filter out contracts, as these are dealt with via cross contract calls
chadoh marked this conversation as resolved.
Show resolved Hide resolved
const sigsNeeded = this.needsNonInvokerSigningBy().filter(id => !id.startsWith('C'));
if (sigsNeeded.length) {
throw new AssembledTransaction.Errors.NeedsMoreSignatures(
"Transaction requires more signatures. " +
`Transaction requires signatures from ${sigsNeeded}. ` +
"See `needsNonInvokerSigningBy` for details.",
);
}
Expand Down Expand Up @@ -760,9 +762,9 @@ export class AssembledTransaction<T> {
"scvVoid"),
)
.map((entry) =>
StrKey.encodeEd25519PublicKey(
entry.credentials().address().address().accountId().ed25519(),
),
Address.fromScAddress(
entry.credentials().address().address(),
).toString(),
),
),
];
Expand All @@ -788,7 +790,8 @@ export class AssembledTransaction<T> {
expiration = (async () =>
(await this.server.getLatestLedger()).sequence + 100)(),
signAuthEntry = this.options.signAuthEntry,
publicKey = this.options.publicKey,
address = this.options.publicKey,
authorizeEntry = stellarBaseAuthorizeEntry,
}: {
/**
* When to set each auth entry to expire. Could be any number of blocks in
Expand All @@ -800,33 +803,40 @@ export class AssembledTransaction<T> {
* Sign all auth entries for this account. Default: the account that
* constructed the transaction
*/
publicKey?: string;
address?: string;
chadoh marked this conversation as resolved.
Show resolved Hide resolved
/**
* You must provide this here if you did not provide one before. Default:
* the `signAuthEntry` function from the `Client` options. Must
* sign things as the given `publicKey`.
* You must provide this here if you did not provide one before and you are not passing `authorizeEntry`. Default: the `signAuthEntry` function from the `Client` options. Must sign things as the given `publicKey`.
Shaptic marked this conversation as resolved.
Show resolved Hide resolved
*/
signAuthEntry?: ClientOptions["signAuthEntry"];

/**
* If you have a pro use-case and need to override the default `authorizeEntry` function, rather than using the one in @stellar/stellar-base, you can do that! Your function needs to take at least the first argument, `entry: xdr.SorobanAuthorizationEntry`, and return a `Promise<xdr.SorobanAuthorizationEntry>`.
*
* Note that you if you pass this, then `signAuthEntry` will be ignored.
*/
authorizeEntry?: typeof stellarBaseAuthorizeEntry;
} = {}): Promise<void> => {
if (!this.built)
throw new Error("Transaction has not yet been assembled or simulated");
const needsNonInvokerSigningBy = this.needsNonInvokerSigningBy();

if (!needsNonInvokerSigningBy) {
throw new AssembledTransaction.Errors.NoUnsignedNonInvokerAuthEntries(
"No unsigned non-invoker auth entries; maybe you already signed?",
);
}
if (needsNonInvokerSigningBy.indexOf(publicKey ?? "") === -1) {
throw new AssembledTransaction.Errors.NoSignatureNeeded(
`No auth entries for public key "${publicKey}"`,
);
}
if (!signAuthEntry) {
throw new AssembledTransaction.Errors.NoSigner(
"You must provide `signAuthEntry` when calling `signAuthEntries`, " +
"or when constructing the `Client` or `AssembledTransaction`",
);
// Likely if we're using a custom authorizeEntry then we know better than the `needsNonInvokerSigningBy` logic.
if (authorizeEntry === stellarBaseAuthorizeEntry) {
const needsNonInvokerSigningBy = this.needsNonInvokerSigningBy();
if (needsNonInvokerSigningBy.length === 0) {
throw new AssembledTransaction.Errors.NoUnsignedNonInvokerAuthEntries(
"No unsigned non-invoker auth entries; maybe you already signed?",
);
}
if (needsNonInvokerSigningBy.indexOf(address ?? "") === -1) {
throw new AssembledTransaction.Errors.NoSignatureNeeded(
`No auth entries for public key "${address}"`,
);
}
if (!signAuthEntry) {
throw new AssembledTransaction.Errors.NoSigner(
"You must provide `signAuthEntry` or a custom `authorizeEntry`"
);
}
}

const rawInvokeHostFunctionOp = this.built
Expand All @@ -836,28 +846,37 @@ export class AssembledTransaction<T> {

// eslint-disable-next-line no-restricted-syntax
for (const [i, entry] of authEntries.entries()) {
// workaround for https://github.com/stellar/js-stellar-sdk/issues/1070
const credentials = xdr.SorobanCredentials.fromXDR(entry.credentials().toXDR())
if (
entry.credentials().switch() !==
credentials.switch() !==
xdr.SorobanCredentialsType.sorobanCredentialsAddress()
Shaptic marked this conversation as resolved.
Show resolved Hide resolved
) {
// if the invoker/source account, then the entry doesn't need explicit
// signature, since the tx envelope is already signed by the source
// account, so only check for sorobanCredentialsAddress
continue; // eslint-disable-line no-continue
}
const pk = StrKey.encodeEd25519PublicKey(
entry.credentials().address().address().accountId().ed25519(),
);
const authEntryAddress = Address.fromScAddress(
credentials.address().address(),
).toString();

// this auth entry needs to be signed by a different account
// (or maybe already was!)
if (pk !== publicKey) continue; // eslint-disable-line no-continue
if (authEntryAddress !== address) continue; // eslint-disable-line no-continue

const sign: typeof signAuthEntry = signAuthEntry ?? Promise.resolve;

// eslint-disable-next-line no-await-in-loop
authEntries[i] = await authorizeEntry(
entry,
async (preimage) =>
Buffer.from(await signAuthEntry(preimage.toXDR("base64")), "base64"),
Buffer.from(
await sign(preimage.toXDR("base64"), {
accountToSign: address,
}),
"base64",
),
await expiration, // eslint-disable-line no-await-in-loop
this.options.networkPassphrase,
);
Expand Down
2 changes: 1 addition & 1 deletion test/e2e/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ yarn.lock
contract-*.txt
.soroban
wasms/specs/*.json
test-contracts
.last_build_hash
53 changes: 25 additions & 28 deletions test/e2e/initialize.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,57 +14,57 @@ dirname="$(CDPATH= cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

echo "###################### Initializing e2e tests ########################"

soroban="$dirname/../../target/bin/soroban"
if [[ -f "$soroban" ]]; then
current=$($soroban --version | head -n 1 | cut -d ' ' -f 2)
stellar="$dirname/../../target/bin/stellar"
if [[ -f "$stellar" ]]; then
current=$($stellar --version | head -n 1 | cut -d ' ' -f 2)
desired=$(cat .cargo/config.toml | grep -oE -- "--version\s+\S+" | awk '{print $2}')
if [[ "$current" != "$desired" ]]; then
echo "Current pinned soroban binary: $current. Desired: $desired. Building soroban binary."
(cd "$dirname/../.." && cargo install_soroban)
echo "Current pinned stellar binary: $current. Desired: $desired. Building stellar binary."
(cd "$dirname/../.." && cargo install_stellar)
else
echo "Using soroban binary from ./target/bin"
echo "Using stellar binary from ./target/bin"
fi
else
echo "Building pinned soroban binary"
(cd "$dirname/../.." && cargo install_soroban)
echo "Building pinned stellar binary"
(cd "$dirname/../.." && cargo install_stellar)
fi

NETWORK_STATUS=$(curl -s -X POST "$SOROBAN_RPC_URL" -H "Content-Type: application/json" -d '{ "jsonrpc": "2.0", "id": 8675309, "method": "getHealth" }' | sed -n 's/.*"status":\s*"\([^"]*\)".*/\1/p')
NETWORK_STATUS=$(curl -s -X POST "$STELLAR_RPC_URL" -H "Content-Type: application/json" -d '{ "jsonrpc": "2.0", "id": 8675309, "method": "getHealth" }' | sed -n 's/.*"status":\s*"\([^"]*\)".*/\1/p')

echo Network
echo " RPC: $SOROBAN_RPC_URL"
echo " Passphrase: \"$SOROBAN_NETWORK_PASSPHRASE\""
echo " RPC: $STELLAR_RPC_URL"
echo " Passphrase: \"$STELLAR_NETWORK_PASSPHRASE\""
echo " Status: $NETWORK_STATUS"

if [[ "$NETWORK_STATUS" != "healthy" ]]; then
echo "Network is not healthy (not running?), exiting"
exit 1
fi

$soroban keys generate $SOROBAN_ACCOUNT
$stellar keys generate $STELLAR_ACCOUNT

# retrieve the contracts using soroban contract init then build them if they dont already exist
# retrieve the contracts using stellar contract init then build them if they dont already exist
# Define directory and WASM file paths
target_dir="$dirname/test-contracts/target/wasm32-unknown-unknown/release"
contracts_dir="$dirname/test-contracts"
repo_url="https://github.com/stellar/soroban-examples.git"
wasm_files=(
"soroban_other_custom_types_contract.wasm"
"soroban_atomic_swap_contract.wasm"
"soroban_token_contract.wasm"
"soroban_increment_contract.wasm"
"hello_world.wasm"
"custom_types.wasm"
"atomic_swap.wasm"
"token.wasm"
"increment.wasm"
"needs_a_signature.wasm"
"this_one_signs.wasm"
)

get_remote_git_hash() {
git ls-remote "$repo_url" HEAD | cut -f1
get_test_contracts_git_hash() {
git ls-files -s "$contracts_dir" | cut -d ' ' -f 2
}

# Get the current git hash
current_hash=$(get_remote_git_hash)
current_hash=$(get_test_contracts_git_hash)

# Check if a stored hash exists
hash_file="$contracts_dir/.last_build_hash"
hash_file="$dirname/.last_build_hash"
if [ -f "$hash_file" ]; then
stored_hash=$(cat "$hash_file")
else
Expand All @@ -75,20 +75,17 @@ fi
all_exist=true
for wasm_file in "${wasm_files[@]}"; do
if [ ! -f "$target_dir/$wasm_file" ]; then
all_exist=false
all_exist=false
break
fi
done

# If any WASM file is missing or the git hash has changed, initialize and build the contracts
if [ "$all_exist" = false ] || [ "$current_hash" != "$stored_hash" ]; then
echo "WASM files are missing or contracts have been updated. Initializing and building contracts..."
# Initialize contracts
$soroban contract init "$dirname/test-contracts" --with-example increment other_custom_types atomic_swap token

# Change directory to test-contracts and build the contracts
cd "$dirname/test-contracts" || { echo "Failed to change directory!"; exit 1; }
$soroban contract build
$stellar contract build
# Save git hash to file
echo "$current_hash" > "$hash_file"
else
Expand Down
Loading
Loading