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
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