Skip to content
37 changes: 34 additions & 3 deletions stellar/PERF.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
# Wraith Stellar Soroban Resource Budget Report

Measured on 2026-06-01 with `soroban-sdk = 22.0.0` resolved to `22.0.11`.
The reusable harness is in `stellar/bench/` and can be re-run with:
Measured on 2026-07-28 with `soroban-sdk = 22.0.11` (host `22.1.3`), post
issue #101 sponsored-announcement PR. The reusable harness is in
`stellar/bench/` and can be re-run with:

```sh
cargo run -p wraith-stellar-bench
cargo run -p wraith-stellar-bench --release
```

Note: bench harness now uses the v2 announcer's `STELLAR_V2_SCHEME_ID` constant
and starts the metadata_len sweep at `1` (the v2 announcer requires a non-empty
view-tag byte), so numbers are directly comparable only to other v2-era runs.

## How to Read the Units

Soroban metering separates execution and ledger access. `instructions` are modeled
Expand Down Expand Up @@ -42,6 +47,10 @@ These baseline numbers were captured before the optimization below.
| stealth-sender | batch_send | batch_size=5 | 807519 | 120229 | 5 | 7 | 1068 | 1416 | 2420 |
| stealth-sender | batch_send | batch_size=10 | 1633634 | 245649 | 5 | 12 | 1068 | 2536 | 4840 |
| stealth-sender | batch_send | batch_size=25 | 4322337 | 690609 | 5 | 27 | 1068 | 5896 | 12100 |
| stealth-sender | sponsored_announce | batch_size=1 | 233608 | 36052 | 7 | 4 | 1164 | 592 | 916 |
| stealth-sender | sponsored_announce | batch_size=5 | 1098506 | 193054 | 11 | 16 | 2060 | 2672 | 2852 |
| stealth-sender | sponsored_announce | batch_size=10 | 2445729 | 491659 | 16 | 31 | 3180 | 5272 | 5272 |
| stealth-sender | sponsored_announce | batch_size=20 | 6018914 | 1430044 | 26 | 61 | 5420 | 10472 | 10112 |
| wraith-names | register | name_len=3 | 59800 | 6269 | 1 | 2 | 104 | 544 | 204 |
| wraith-names | register | name_len=32 | 61413 | 6327 | 1 | 2 | 104 | 572 | 232 |
| wraith-names | resolve | hit | 46120 | 5537 | 1 | 0 | 476 | 0 | 0 |
Expand Down Expand Up @@ -179,6 +188,28 @@ Mermaid source: [`bench/data/crossover-chart.md`](bench/data/crossover-chart.md)
Expected savings: low to medium; correctness and readability should be
preserved.

## Sponsored Announcement Cost

The sponsored rows are measured with the bench harness's
`stealth_announcer::STELLAR_V2_SCHEME_ID = 2` and a single authenticated
sender reused across all entries of a bundle (the harness's
`Vec::contains` dedup collapses them). Per-entry cost is the row total
divided by `batch_size`.

| Batch size | Instructions/entry | Mem bytes/entry | Event bytes/entry |
|---:|---:|---:|---:|
| 1 | 233980 | 36242 | 1096 |
| 5 | 173514 | 26141 | 587 |
| 10 | 169615 | 25909 | 524 |
| 20 | 173120 | 27511 | 492 |

Per-entry cost falls rapidly and bottoms out around 169–173k instructions at
`batch_size=10..20`. The `batch_size=1` row carries the full
function-call overhead, so it is not a useful operational measurement: in
production, sponsored announcements are always bundled to amortize the
per-op envelope cost. Operators should target `batch_size >= 5` where the
per-entry envelope drops ~26% vs single-shot.

## Concrete Diff Suggestions

1. Batch send: add a dedicated announcer batch API and invoke it once after all
Expand Down
46 changes: 42 additions & 4 deletions stellar/bench/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ struct Row {
fn main() {
let mut rows = std::vec::Vec::new();

for metadata_len in [0u32, 32, 256, 1024, 4096] {
for metadata_len in [1u32, 32, 256, 1024, 4096] {
rows.push(measure(
"stealth-announcer",
"announce",
Expand All @@ -33,7 +33,7 @@ fn main() {
let contract_id = env.register(StealthAnnouncerContract, ());
let client = StealthAnnouncerContractClient::new(env, &contract_id);
client.announce(
&1,
&stealth_announcer::STELLAR_V2_SCHEME_ID,
&Address::generate(env),
&BytesN::from_array(env, &[7u8; 32]),
&bytes(env, metadata_len, 9),
Expand Down Expand Up @@ -84,7 +84,7 @@ fn main() {
&sender,
&token,
&100,
&1,
&stealth_announcer::STELLAR_V2_SCHEME_ID,
&Address::generate(env),
&BytesN::from_array(env, &[3u8; 32]),
&bytes(env, 32, 4),
Expand Down Expand Up @@ -115,7 +115,45 @@ fn main() {
metadatas.push_back(bytes(env, 32, i as u8));
amounts.push_back(100);
}
client.batch_send(&sender, &token, &1, &addresses, &keys, &metadatas, &amounts);
client.batch_send(
&sender,
&token,
&stealth_announcer::STELLAR_V2_SCHEME_ID,
&addresses,
&keys,
&metadatas,
&amounts,
);
},
));
}

for batch_size in [1u32, 5, 10, 20] {
rows.push(measure(
"stealth-sender",
"sponsored_announce",
format!("batch_size={batch_size}"),
|env| {
env.mock_all_auths();
let sender_contract_id = env.register(StealthSenderContract, ());
let announcer_id = env.register(StealthAnnouncerContract, ());
let client = StealthSenderContractClient::new(env, &sender_contract_id);
client.init(&announcer_id, &None, &None, &0);
let (token, sender) = funded_token(env, true);
let sponsor = Address::generate(env);
let mut entries: SorobanVec<stealth_sender::SponsoredEntry> = vec![env];
for i in 0..batch_size {
entries.push_back(stealth_sender::SponsoredEntry {
sender: sender.clone(),
token: token.clone(),
amount: 100,
scheme_id: stealth_announcer::STELLAR_V2_SCHEME_ID,
stealth_address: Address::generate(env),
ephemeral_pub_key: BytesN::from_array(env, &[i as u8; 32]),
metadata: bytes(env, 32, i as u8),
});
}
client.sponsored_announce(&sponsor, &entries);
},
));
}
Expand Down
83 changes: 75 additions & 8 deletions stellar/bindings/typescript/stealth-sender/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ if (typeof window !== "undefined") {
/**
* Storage keys.
*/
export type DataKey = {tag: "Announcer", values: void};
export type DataKey = {tag: "Announcer", values: void} | {tag: "AssetPolicy", values: void} | {tag: "FeeRecipient", values: void} | {tag: "FeeBasisPoints", values: void};

/**
* Errors that the sender contract can produce.
Expand All @@ -53,17 +53,70 @@ export const SenderError = {
/**
* The batch input vectors have mismatched lengths.
*/
3: {message:"LengthMismatch"}
3: {message:"LengthMismatch"},
/**
* The token is not allowed by the asset policy.
*/
4: {message:"TokenNotAllowed"},
/**
* The fee configuration is invalid (e.g. fee > 50 bps, or fee > 0 with no recipient).
*/
5: {message:"InvalidFeeConfig"},
/**
* The sponsored announcement batch exceeds the maximum entry count.
*/
6: {message:"SponsoredBatchTooLarge"}
}


/**
* A token transfer and announcement authenticated by its sender.
*/
export interface SponsoredEntry {
amount: i128;
ephemeral_pub_key: Buffer;
metadata: Buffer;
scheme_id: u32;
sender: string;
stealth_address: string;
token: string;
}


/**
* Wraith Protocol standard metric event schema.
*
* All Wraith contracts emit metric events using this structure to enable
* standardized off-chain observability and monitoring.
*/
export interface WraithMetricEvent {
/**
* Contract identifier (e.g., "stealth-registry", "stealth-sender")
*/
contract: string;
/**
* Optional dimensions for filtering/grouping (e.g., token_address, scheme_id)
*/
dimensions: Array<readonly [string, any]>;
/**
* Metric name (e.g., "register_count", "send_volume")
*/
metric_name: string;
/**
* Numeric value of the metric
*/
value: i128;
}

export interface Client {
/**
* Construct and simulate a init transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object.
* Initialise the contract by storing the announcer address.
* Initialise the contract by storing the announcer address, optional asset policy,
* and optional protocol fee configuration.
*
* Must be called exactly once before any `send` or `batch_send`.
*/
init: ({announcer}: {announcer: string}, options?: MethodOptions) => Promise<AssembledTransaction<Result<void>>>
init: ({announcer, asset_policy, fee_recipient, fee_basis_points}: {announcer: string, asset_policy: Option<string>, fee_recipient: Option<string>, fee_basis_points: u32}, options?: MethodOptions) => Promise<AssembledTransaction<Result<void>>>

/**
* Construct and simulate a send transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object.
Expand All @@ -89,6 +142,16 @@ export interface Client {
*/
batch_send: ({sender, token, scheme_id, stealth_addresses, ephemeral_pub_keys, metadatas, amounts}: {sender: string, token: string, scheme_id: u32, stealth_addresses: Array<string>, ephemeral_pub_keys: Array<Buffer>, metadatas: Array<Buffer>, amounts: Array<i128>}, options?: MethodOptions) => Promise<AssembledTransaction<Result<void>>>

/**
* Construct and simulate a sponsored_announce transaction. Returns an `AssembledTransaction` object which will have a `result` field containing the result of the simulation. If this transaction changes contract state, you will need to call `signAndSend()` on the returned object.
* Transfer tokens and emit announcements for entries paid for by a sponsor.
*
* The sponsor and every entry sender must authorise the invocation. The
* sponsor pays the transaction fee through Stellar fee-bump mechanics;
* entry senders remain responsible for their own token transfers.
*/
sponsored_announce: ({sponsor, entries}: {sponsor: string, entries: Array<SponsoredEntry>}, options?: MethodOptions) => Promise<AssembledTransaction<Result<void>>>

}
export class Client extends ContractClient {
static async deploy<T = Client>(
Expand All @@ -107,17 +170,21 @@ export class Client extends ContractClient {
}
constructor(public readonly options: ContractClientOptions) {
super(
new ContractSpec([ "AAAAAAAAAHlJbml0aWFsaXNlIHRoZSBjb250cmFjdCBieSBzdG9yaW5nIHRoZSBhbm5vdW5jZXIgYWRkcmVzcy4KCk11c3QgYmUgY2FsbGVkIGV4YWN0bHkgb25jZSBiZWZvcmUgYW55IGBzZW5kYCBvciBgYmF0Y2hfc2VuZGAuAAAAAAAABGluaXQAAAABAAAAAAAAAAlhbm5vdW5jZXIAAAAAAAATAAAAAQAAA+kAAAPtAAAAAAAAB9AAAAALU2VuZGVyRXJyb3IA",
new ContractSpec([ "AAAAAgAAAA1TdG9yYWdlIGtleXMuAAAAAAAAAAAAAAdEYXRhS2V5AAAAAAQAAAAAAAAANlRoZSBhZGRyZXNzIG9mIHRoZSBkZXBsb3llZCBTdGVhbHRoQW5ub3VuY2VyIGNvbnRyYWN0LgAAAAAACUFubm91bmNlcgAAAAAAAAAAAAAuT3B0aW9uYWwgYWRkcmVzcyBvZiB0aGUgYXNzZXQgcG9saWN5IGNvbnRyYWN0LgAAAAAAC0Fzc2V0UG9saWN5AAAAAAAAAAAvT3B0aW9uYWwgYWRkcmVzcyBvZiB0aGUgcHJvdG9jb2wgZmVlIHJlY2lwaWVudC4AAAAADEZlZVJlY2lwaWVudAAAAAAAAAA4UHJvdG9jb2wgZmVlIGluIGJhc2lzIHBvaW50cyAobWF4IDUwIGJwcywgMCA9IGRpc2FibGVkKS4AAAAORmVlQmFzaXNQb2ludHMAAA==",
"AAAABAAAACxFcnJvcnMgdGhhdCB0aGUgc2VuZGVyIGNvbnRyYWN0IGNhbiBwcm9kdWNlLgAAAAAAAAALU2VuZGVyRXJyb3IAAAAABgAAACpUaGUgY29udHJhY3QgaGFzIGFscmVhZHkgYmVlbiBpbml0aWFsaXNlZC4AAAAAABJBbHJlYWR5SW5pdGlhbGl6ZWQAAAAAAAEAAAAqVGhlIGNvbnRyYWN0IGhhcyBub3QgYmVlbiBpbml0aWFsaXNlZCB5ZXQuAAAAAAAOTm90SW5pdGlhbGl6ZWQAAAAAAAIAAAAwVGhlIGJhdGNoIGlucHV0IHZlY3RvcnMgaGF2ZSBtaXNtYXRjaGVkIGxlbmd0aHMuAAAADkxlbmd0aE1pc21hdGNoAAAAAAADAAAALVRoZSB0b2tlbiBpcyBub3QgYWxsb3dlZCBieSB0aGUgYXNzZXQgcG9saWN5LgAAAAAAAA9Ub2tlbk5vdEFsbG93ZWQAAAAABAAAAFNUaGUgZmVlIGNvbmZpZ3VyYXRpb24gaXMgaW52YWxpZCAoZS5nLiBmZWUgPiA1MCBicHMsIG9yIGZlZSA+IDAgd2l0aCBubyByZWNpcGllbnQpLgAAAAAQSW52YWxpZEZlZUNvbmZpZwAAAAUAAABBVGhlIHNwb25zb3JlZCBhbm5vdW5jZW1lbnQgYmF0Y2ggZXhjZWVkcyB0aGUgbWF4aW11bSBlbnRyeSBjb3VudC4AAAAAAAAWU3BvbnNvcmVkQmF0Y2hUb29MYXJnZQAAAAAABg==",
"AAAAAQAAAD5BIHRva2VuIHRyYW5zZmVyIGFuZCBhbm5vdW5jZW1lbnQgYXV0aGVudGljYXRlZCBieSBpdHMgc2VuZGVyLgAAAAAAAAAAAA5TcG9uc29yZWRFbnRyeQAAAAAABwAAAAAAAAAGYW1vdW50AAAAAAALAAAAAAAAABFlcGhlbWVyYWxfcHViX2tleQAAAAAAA+4AAAAgAAAAAAAAAAhtZXRhZGF0YQAAAA4AAAAAAAAACXNjaGVtZV9pZAAAAAAAAAQAAAAAAAAABnNlbmRlcgAAAAAAEwAAAAAAAAAPc3RlYWx0aF9hZGRyZXNzAAAAABMAAAAAAAAABXRva2VuAAAAAAAAEw==",
"AAAAAAAAALlJbml0aWFsaXNlIHRoZSBjb250cmFjdCBieSBzdG9yaW5nIHRoZSBhbm5vdW5jZXIgYWRkcmVzcywgb3B0aW9uYWwgYXNzZXQgcG9saWN5LAphbmQgb3B0aW9uYWwgcHJvdG9jb2wgZmVlIGNvbmZpZ3VyYXRpb24uCgpNdXN0IGJlIGNhbGxlZCBleGFjdGx5IG9uY2UgYmVmb3JlIGFueSBgc2VuZGAgb3IgYGJhdGNoX3NlbmRgLgAAAAAAAARpbml0AAAABAAAAAAAAAAJYW5ub3VuY2VyAAAAAAAAEwAAAAAAAAAMYXNzZXRfcG9saWN5AAAD6AAAABMAAAAAAAAADWZlZV9yZWNpcGllbnQAAAAAAAPoAAAAEwAAAAAAAAAQZmVlX2Jhc2lzX3BvaW50cwAAAAQAAAABAAAD6QAAA+0AAAAAAAAH0AAAAAtTZW5kZXJFcnJvcgA=",
"AAAAAAAAAglUcmFuc2ZlciB0b2tlbnMgdG8gYSBzdGVhbHRoIGFkZHJlc3MgYW5kIGVtaXQgYW4gYW5ub3VuY2VtZW50LgoKIyBBcmd1bWVudHMKKiBgc2VuZGVyYCAgICAgICAgICAgIC0gVGhlIGFkZHJlc3Mgc2VuZGluZyBmdW5kcyAobXVzdCBhdXRob3Jpc2UpLgoqIGB0b2tlbmAgICAgICAgICAgICAgLSBTQUMgdG9rZW4gY29udHJhY3QgYWRkcmVzcyAod29ya3MgZm9yIG5hdGl2ZSBYTE0gdG9vKS4KKiBgYW1vdW50YCAgICAgICAgICAgIC0gQW1vdW50IG9mIHRva2VucyB0byB0cmFuc2Zlci4KKiBgc2NoZW1lX2lkYCAgICAgICAgIC0gU3RlYWx0aCBhZGRyZXNzIHNjaGVtZSBpZGVudGlmaWVyLgoqIGBzdGVhbHRoX2FkZHJlc3NgICAgLSBUaGUgZGVyaXZlZCBvbmUtdGltZSBzdGVhbHRoIGFkZHJlc3MuCiogYGVwaGVtZXJhbF9wdWJfa2V5YCAtIEVwaGVtZXJhbCBwdWJsaWMga2V5IGZvciB0aGUgcmVjaXBpZW50IHRvIHNjYW4uCiogYG1ldGFkYXRhYCAgICAgICAgICAtIEV4dHJhIGRhdGEgKGUuZy4gdmlldyB0YWcpLgAAAAAAAARzZW5kAAAABwAAAAAAAAAGc2VuZGVyAAAAAAATAAAAAAAAAAV0b2tlbgAAAAAAABMAAAAAAAAABmFtb3VudAAAAAAACwAAAAAAAAAJc2NoZW1lX2lkAAAAAAAABAAAAAAAAAAPc3RlYWx0aF9hZGRyZXNzAAAAABMAAAAAAAAAEWVwaGVtZXJhbF9wdWJfa2V5AAAAAAAD7gAAACAAAAAAAAAACG1ldGFkYXRhAAAADgAAAAEAAAPpAAAD7QAAAAAAAAfQAAAAC1NlbmRlckVycm9yAA==",
"AAAAAgAAAA1TdG9yYWdlIGtleXMuAAAAAAAAAAAAAAdEYXRhS2V5AAAAAAEAAAAAAAAANlRoZSBhZGRyZXNzIG9mIHRoZSBkZXBsb3llZCBTdGVhbHRoQW5ub3VuY2VyIGNvbnRyYWN0LgAAAAAACUFubm91bmNlcgAAAA==",
"AAAAAAAAAJxCYXRjaCB2ZXJzaW9uIG9mIGBzZW5kYCDigJQgdHJhbnNmZXJzIHRva2VucyB0byBtdWx0aXBsZSBzdGVhbHRoIGFkZHJlc3NlcwphbmQgZW1pdHMgYW4gYW5ub3VuY2VtZW50IGZvciBlYWNoLgoKQWxsIGlucHV0IHZlY3RvcnMgbXVzdCBoYXZlIHRoZSBzYW1lIGxlbmd0aC4AAAAKYmF0Y2hfc2VuZAAAAAAABwAAAAAAAAAGc2VuZGVyAAAAAAATAAAAAAAAAAV0b2tlbgAAAAAAABMAAAAAAAAACXNjaGVtZV9pZAAAAAAAAAQAAAAAAAAAEXN0ZWFsdGhfYWRkcmVzc2VzAAAAAAAD6gAAABMAAAAAAAAAEmVwaGVtZXJhbF9wdWJfa2V5cwAAAAAD6gAAA+4AAAAgAAAAAAAAAAltZXRhZGF0YXMAAAAAAAPqAAAADgAAAAAAAAAHYW1vdW50cwAAAAPqAAAACwAAAAEAAAPpAAAD7QAAAAAAAAfQAAAAC1NlbmRlckVycm9yAA==",
"AAAABAAAACxFcnJvcnMgdGhhdCB0aGUgc2VuZGVyIGNvbnRyYWN0IGNhbiBwcm9kdWNlLgAAAAAAAAALU2VuZGVyRXJyb3IAAAAAAwAAACpUaGUgY29udHJhY3QgaGFzIGFscmVhZHkgYmVlbiBpbml0aWFsaXNlZC4AAAAAABJBbHJlYWR5SW5pdGlhbGl6ZWQAAAAAAAEAAAAqVGhlIGNvbnRyYWN0IGhhcyBub3QgYmVlbiBpbml0aWFsaXNlZCB5ZXQuAAAAAAAOTm90SW5pdGlhbGl6ZWQAAAAAAAIAAAAwVGhlIGJhdGNoIGlucHV0IHZlY3RvcnMgaGF2ZSBtaXNtYXRjaGVkIGxlbmd0aHMuAAAADkxlbmd0aE1pc21hdGNoAAAAAAAD" ]),
"AAAAAAAAARVUcmFuc2ZlciB0b2tlbnMgYW5kIGVtaXQgYW5ub3VuY2VtZW50cyBmb3IgZW50cmllcyBwYWlkIGZvciBieSBhIHNwb25zb3IuCgpUaGUgc3BvbnNvciBhbmQgZXZlcnkgZW50cnkgc2VuZGVyIG11c3QgYXV0aG9yaXNlIHRoZSBpbnZvY2F0aW9uLiBUaGUKc3BvbnNvciBwYXlzIHRoZSB0cmFuc2FjdGlvbiBmZWUgdGhyb3VnaCBTdGVsbGFyIGZlZS1idW1wIG1lY2hhbmljczsKZW50cnkgc2VuZGVycyByZW1haW4gcmVzcG9uc2libGUgZm9yIHRoZWlyIG93biB0b2tlbiB0cmFuc2ZlcnMuAAAAAAAAEnNwb25zb3JlZF9hbm5vdW5jZQAAAAAAAgAAAAAAAAAHc3BvbnNvcgAAAAATAAAAAAAAAAdlbnRyaWVzAAAAA+oAAAfQAAAADlNwb25zb3JlZEVudHJ5AAAAAAABAAAD6QAAA+0AAAAAAAAH0AAAAAtTZW5kZXJFcnJvcgA=",
"AAAAAQAAAKpXcmFpdGggUHJvdG9jb2wgc3RhbmRhcmQgbWV0cmljIGV2ZW50IHNjaGVtYS4KCkFsbCBXcmFpdGggY29udHJhY3RzIGVtaXQgbWV0cmljIGV2ZW50cyB1c2luZyB0aGlzIHN0cnVjdHVyZSB0byBlbmFibGUKc3RhbmRhcmRpemVkIG9mZi1jaGFpbiBvYnNlcnZhYmlsaXR5IGFuZCBtb25pdG9yaW5nLgAAAAAAAAAAABFXcmFpdGhNZXRyaWNFdmVudAAAAAAAAAQAAABAQ29udHJhY3QgaWRlbnRpZmllciAoZS5nLiwgInN0ZWFsdGgtcmVnaXN0cnkiLCAic3RlYWx0aC1zZW5kZXIiKQAAAAhjb250cmFjdAAAABEAAABLT3B0aW9uYWwgZGltZW5zaW9ucyBmb3IgZmlsdGVyaW5nL2dyb3VwaW5nIChlLmcuLCB0b2tlbl9hZGRyZXNzLCBzY2hlbWVfaWQpAAAAAApkaW1lbnNpb25zAAAAAAPqAAAD7QAAAAIAAAARAAAAAAAAADNNZXRyaWMgbmFtZSAoZS5nLiwgInJlZ2lzdGVyX2NvdW50IiwgInNlbmRfdm9sdW1lIikAAAAAC21ldHJpY19uYW1lAAAAABEAAAAbTnVtZXJpYyB2YWx1ZSBvZiB0aGUgbWV0cmljAAAAAAV2YWx1ZQAAAAAAAAs=" ]),
options
)
}
public readonly fromJSON = {
init: this.txFromJSON<Result<void>>,
send: this.txFromJSON<Result<void>>,
batch_send: this.txFromJSON<Result<void>>
batch_send: this.txFromJSON<Result<void>>,
sponsored_announce: this.txFromJSON<Result<void>>
}
}
40 changes: 40 additions & 0 deletions stellar/integration-tests/sponsored.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//! Futurenet verification for a sponsored announcement transaction.
//!
//! Submit a fee-bumped `sponsored_announce` transaction with the Stellar CLI or
//! client SDK, then run this test with `FUTURENET_TX_HASH` and
//! `FUTURENET_SPONSOR_ADDRESS` set. The test verifies the network recorded the
//! sponsor as the transaction fee source.

use reqwest::Client;
use serde::Deserialize;

#[derive(Debug, Deserialize)]
struct Transaction {
successful: bool,
fee_account: String,
}

#[tokio::test]
#[ignore = "requires a submitted futurenet fee-bumped transaction"]
async fn sponsored_announcement_fee_is_charged_to_sponsor() {
let transaction_hash = std::env::var("FUTURENET_TX_HASH")
.expect("FUTURENET_TX_HASH must identify the submitted transaction");
let sponsor = std::env::var("FUTURENET_SPONSOR_ADDRESS")
.expect("FUTURENET_SPONSOR_ADDRESS must identify the fee-bump source");
let horizon = std::env::var("FUTURENET_HORIZON_URL")
.unwrap_or_else(|_| "https://horizon-futurenet.stellar.org".to_owned());

let transaction: Transaction = Client::new()
.get(format!("{horizon}/transactions/{transaction_hash}"))
.send()
.await
.expect("Horizon request failed")
.error_for_status()
.expect("Horizon rejected the transaction lookup")
.json()
.await
.expect("Horizon returned invalid transaction JSON");

assert!(transaction.successful, "sponsored transaction failed");
assert_eq!(transaction.fee_account, sponsor);
}
Loading
Loading