Skip to content

Commit e410245

Browse files
Include the token interface in the stellar asset interface (#1427)
### What Include the token interface in the stellar asset interface, and therefore also in the client. ### Why The separation of the token functions from the stellar asset interface and client was originally done to ensure that developers didn't use the stellar asset client in situations where they really should be using the token client, to interface with tokens of any kind. However, most of the time when people discover the stellar asset client is missing those functions, it creates confusion. It always requires an explanation. I think this is largely because in all other situations clients contain all the functions of a contract. After discussing this with @tomerweller, who was the most recent person to run into this, I think while we had good intentions, it has landed with different impact than we thought it'd have. ### How The approach taken to the changes was to manually copy-paste the functions across the two interfaces. A macro could be used here but because any macro used here would need to interact well with the attribute macros in use on the traits, the complexity isn't worth it. It is unlikely the two will get out of sync because tests have been added to ensure the token spec is a subset of the stellar asset spec. As part of the change I needed to fix a minor bug where the contractspec macro still wrote out a static variable even when exporting was disabled. Normally we don't allow more than one exported function with the same name per module. We have exporting turned off in this code, but the code was still generating the variable to export resulting in a collision since both interfaces contain functions with the same names.
1 parent 8053660 commit e410245

File tree

3 files changed

+264
-21
lines changed

3 files changed

+264
-21
lines changed

soroban-sdk-macros/src/derive_spec_fn.rs

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,15 @@ pub fn derive_fn_spec(
157157
return Err(quote! { #(#compile_errors)* });
158158
}
159159

160-
let export_attr = if export {
161-
Some(quote! { #[cfg_attr(target_family = "wasm", link_section = "contractspecv0")] })
160+
let exported = if export {
161+
Some(quote! {
162+
#[doc(hidden)]
163+
#[allow(non_snake_case)]
164+
#[allow(non_upper_case_globals)]
165+
#(#attrs)*
166+
#[cfg_attr(target_family = "wasm", link_section = "contractspecv0")]
167+
pub static #spec_ident: [u8; #spec_xdr_len] = #ty::#spec_fn_ident();
168+
})
162169
} else {
163170
None
164171
};
@@ -171,12 +178,7 @@ pub fn derive_fn_spec(
171178

172179
// Generated code.
173180
Ok(quote! {
174-
#[doc(hidden)]
175-
#[allow(non_snake_case)]
176-
#[allow(non_upper_case_globals)]
177-
#(#attrs)*
178-
#export_attr
179-
pub static #spec_ident: [u8; #spec_xdr_len] = #ty::#spec_fn_ident();
181+
#exported
180182

181183
impl #ty {
182184
#[allow(non_snake_case)]
Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,29 @@
11
use crate as soroban_sdk;
2+
use std::collections::HashSet;
23

34
use soroban_sdk::{
4-
token::{StellarAssetSpec, SPEC_XDR_INPUT, SPEC_XDR_LEN},
5+
token::{
6+
StellarAssetSpec, TokenSpec, STELLAR_ASSET_SPEC_XDR_INPUT, STELLAR_ASSET_SPEC_XDR_LEN,
7+
TOKEN_SPEC_XDR_INPUT, TOKEN_SPEC_XDR_LEN,
8+
},
59
xdr::{Error, Limited, Limits, ReadXdr, ScSpecEntry},
610
};
711

812
extern crate std;
13+
#[test]
14+
fn test_stellar_asset_spec_xdr_len() {
15+
let len = STELLAR_ASSET_SPEC_XDR_INPUT
16+
.iter()
17+
.fold(0usize, |sum, x| sum + x.len());
18+
assert_eq!(STELLAR_ASSET_SPEC_XDR_LEN, len);
19+
}
920

1021
#[test]
11-
fn test_spec_xdr_len() {
12-
let len = SPEC_XDR_INPUT.iter().fold(0usize, |sum, x| sum + x.len());
13-
assert_eq!(SPEC_XDR_LEN, len);
22+
fn test_token_spec_xdr_len() {
23+
let len = TOKEN_SPEC_XDR_INPUT
24+
.iter()
25+
.fold(0usize, |sum, x| sum + x.len());
26+
assert_eq!(TOKEN_SPEC_XDR_LEN, len);
1427
}
1528

1629
#[test]
@@ -22,3 +35,37 @@ fn test_spec_xdr() -> Result<(), Error> {
2235
}
2336
Ok(())
2437
}
38+
39+
#[test]
40+
fn test_token_spec_xdr() -> Result<(), Error> {
41+
let xdr = TokenSpec::spec_xdr();
42+
let cursor = std::io::Cursor::new(xdr);
43+
for spec_entry in ScSpecEntry::read_xdr_iter(&mut Limited::new(cursor, Limits::none())) {
44+
spec_entry?;
45+
}
46+
Ok(())
47+
}
48+
49+
#[test]
50+
fn test_stellar_asset_spec_includes_token_spec() -> Result<(), Error> {
51+
// Read all TokenSpec entries
52+
let token_xdr = TokenSpec::spec_xdr();
53+
let token_cursor = std::io::Cursor::new(token_xdr);
54+
let token_entries: HashSet<ScSpecEntry> =
55+
ScSpecEntry::read_xdr_iter(&mut Limited::new(token_cursor, Limits::none()))
56+
.collect::<Result<HashSet<_>, _>>()?;
57+
58+
// Read all StellarAssetSpec entries
59+
let stellar_asset_xdr = StellarAssetSpec::spec_xdr();
60+
let stellar_asset_cursor = std::io::Cursor::new(stellar_asset_xdr);
61+
let stellar_asset_entries: HashSet<ScSpecEntry> =
62+
ScSpecEntry::read_xdr_iter(&mut Limited::new(stellar_asset_cursor, Limits::none()))
63+
.collect::<Result<HashSet<_>, _>>()?;
64+
65+
// Check that the token entries are a subset of stellar entries
66+
assert!(
67+
token_entries.is_subset(&stellar_asset_entries),
68+
"StellarAssetSpec is missing entries from TokenSpec"
69+
);
70+
Ok(())
71+
}

soroban-sdk/src/token.rs

Lines changed: 203 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ pub use TokenClient as Client;
8080
/// There are no functions in the token interface for minting tokens. Minting is
8181
/// an administrative function that can differ significantly from one token to
8282
/// the next.
83-
#[contractspecfn(name = "StellarAssetSpec", export = false)]
83+
#[contractspecfn(name = "TokenSpec", export = false)]
8484
#[contractclient(crate_path = "crate", name = "TokenClient")]
8585
pub trait TokenInterface {
8686
/// Returns the allowance for `spender` to transfer from `from`.
@@ -230,11 +230,205 @@ pub trait TokenInterface {
230230
fn symbol(env: Env) -> String;
231231
}
232232

233+
/// Spec contains the contract spec of Token contracts.
234+
#[doc(hidden)]
235+
pub struct TokenSpec;
236+
237+
pub(crate) const TOKEN_SPEC_XDR_INPUT: &[&[u8]] = &[
238+
&TokenSpec::spec_xdr_allowance(),
239+
&TokenSpec::spec_xdr_approve(),
240+
&TokenSpec::spec_xdr_balance(),
241+
&TokenSpec::spec_xdr_burn(),
242+
&TokenSpec::spec_xdr_burn_from(),
243+
&TokenSpec::spec_xdr_decimals(),
244+
&TokenSpec::spec_xdr_name(),
245+
&TokenSpec::spec_xdr_symbol(),
246+
&TokenSpec::spec_xdr_transfer(),
247+
&TokenSpec::spec_xdr_transfer_from(),
248+
];
249+
250+
pub(crate) const TOKEN_SPEC_XDR_LEN: usize = 4716;
251+
252+
impl TokenSpec {
253+
/// Returns the XDR spec for the Token contract.
254+
pub const fn spec_xdr() -> [u8; TOKEN_SPEC_XDR_LEN] {
255+
let input = TOKEN_SPEC_XDR_INPUT;
256+
// Concatenate all XDR for each item that makes up the token spec.
257+
let mut output = [0u8; TOKEN_SPEC_XDR_LEN];
258+
let mut input_i = 0;
259+
let mut output_i = 0;
260+
while input_i < input.len() {
261+
let subinput = input[input_i];
262+
let mut subinput_i = 0;
263+
while subinput_i < subinput.len() {
264+
output[output_i] = subinput[subinput_i];
265+
output_i += 1;
266+
subinput_i += 1;
267+
}
268+
input_i += 1;
269+
}
270+
271+
// Check that the numbers of bytes written is equal to the number of bytes
272+
// expected in the output.
273+
if output_i != output.len() {
274+
panic!("unexpected output length",);
275+
}
276+
277+
output
278+
}
279+
}
280+
233281
/// Interface for admin capabilities for Token contracts, such as the Stellar
234282
/// Asset Contract.
235283
#[contractspecfn(name = "StellarAssetSpec", export = false)]
236284
#[contractclient(crate_path = "crate", name = "StellarAssetClient")]
237285
pub trait StellarAssetInterface {
286+
/// Returns the allowance for `spender` to transfer from `from`.
287+
///
288+
/// The amount returned is the amount that spender is allowed to transfer
289+
/// out of from's balance. When the spender transfers amounts, the allowance
290+
/// will be reduced by the amount transferred.
291+
///
292+
/// # Arguments
293+
///
294+
/// * `from` - The address holding the balance of tokens to be drawn from.
295+
/// * `spender` - The address spending the tokens held by `from`.
296+
fn allowance(env: Env, from: Address, spender: Address) -> i128;
297+
298+
/// Set the allowance by `amount` for `spender` to transfer/burn from
299+
/// `from`.
300+
///
301+
/// The amount set is the amount that spender is approved to transfer out of
302+
/// from's balance. The spender will be allowed to transfer amounts, and
303+
/// when an amount is transferred the allowance will be reduced by the
304+
/// amount transferred.
305+
///
306+
/// # Arguments
307+
///
308+
/// * `from` - The address holding the balance of tokens to be drawn from.
309+
/// * `spender` - The address being authorized to spend the tokens held by
310+
/// `from`.
311+
/// * `amount` - The tokens to be made available to `spender`.
312+
/// * `expiration_ledger` - The ledger number where this allowance expires. Cannot
313+
/// be less than the current ledger number unless the amount is being set to 0.
314+
/// An expired entry (where expiration_ledger < the current ledger number)
315+
/// should be treated as a 0 amount allowance.
316+
///
317+
/// # Events
318+
///
319+
/// Emits an event with topics `["approve", from: Address,
320+
/// spender: Address], data = [amount: i128, expiration_ledger: u32]`
321+
fn approve(env: Env, from: Address, spender: Address, amount: i128, expiration_ledger: u32);
322+
323+
/// Returns the balance of `id`.
324+
///
325+
/// # Arguments
326+
///
327+
/// * `id` - The address for which a balance is being queried. If the
328+
/// address has no existing balance, returns 0.
329+
fn balance(env: Env, id: Address) -> i128;
330+
331+
/// Transfer `amount` from `from` to `to`.
332+
///
333+
/// # Arguments
334+
///
335+
/// * `from` - The address holding the balance of tokens which will be
336+
/// withdrawn from.
337+
/// * `to` - The address which will receive the transferred tokens.
338+
/// * `amount` - The amount of tokens to be transferred.
339+
///
340+
/// # Events
341+
///
342+
/// Emits an event with topics `["transfer", from: Address, to: Address],
343+
/// data = amount: i128`
344+
fn transfer(env: Env, from: Address, to: Address, amount: i128);
345+
346+
/// Transfer `amount` from `from` to `to`, consuming the allowance that
347+
/// `spender` has on `from`'s balance. Authorized by spender
348+
/// (`spender.require_auth()`).
349+
///
350+
/// The spender will be allowed to transfer the amount from from's balance
351+
/// if the amount is less than or equal to the allowance that the spender
352+
/// has on the from's balance. The spender's allowance on from's balance
353+
/// will be reduced by the amount.
354+
///
355+
/// # Arguments
356+
///
357+
/// * `spender` - The address authorizing the transfer, and having its
358+
/// allowance consumed during the transfer.
359+
/// * `from` - The address holding the balance of tokens which will be
360+
/// withdrawn from.
361+
/// * `to` - The address which will receive the transferred tokens.
362+
/// * `amount` - The amount of tokens to be transferred.
363+
///
364+
/// # Events
365+
///
366+
/// Emits an event with topics `["transfer", from: Address, to: Address],
367+
/// data = amount: i128`
368+
fn transfer_from(env: Env, spender: Address, from: Address, to: Address, amount: i128);
369+
370+
/// Burn `amount` from `from`.
371+
///
372+
/// Reduces from's balance by the amount, without transferring the balance
373+
/// to another holder's balance.
374+
///
375+
/// # Arguments
376+
///
377+
/// * `from` - The address holding the balance of tokens which will be
378+
/// burned from.
379+
/// * `amount` - The amount of tokens to be burned.
380+
///
381+
/// # Events
382+
///
383+
/// Emits an event with topics `["burn", from: Address], data = amount:
384+
/// i128`
385+
fn burn(env: Env, from: Address, amount: i128);
386+
387+
/// Burn `amount` from `from`, consuming the allowance of `spender`.
388+
///
389+
/// Reduces from's balance by the amount, without transferring the balance
390+
/// to another holder's balance.
391+
///
392+
/// The spender will be allowed to burn the amount from from's balance, if
393+
/// the amount is less than or equal to the allowance that the spender has
394+
/// on the from's balance. The spender's allowance on from's balance will be
395+
/// reduced by the amount.
396+
///
397+
/// # Arguments
398+
///
399+
/// * `spender` - The address authorizing the burn, and having its allowance
400+
/// consumed during the burn.
401+
/// * `from` - The address holding the balance of tokens which will be
402+
/// burned from.
403+
/// * `amount` - The amount of tokens to be burned.
404+
///
405+
/// # Events
406+
///
407+
/// Emits an event with topics `["burn", from: Address], data = amount:
408+
/// i128`
409+
fn burn_from(env: Env, spender: Address, from: Address, amount: i128);
410+
411+
/// Returns the number of decimals used to represent amounts of this token.
412+
///
413+
/// # Panics
414+
///
415+
/// If the contract has not yet been initialized.
416+
fn decimals(env: Env) -> u32;
417+
418+
/// Returns the name for this token.
419+
///
420+
/// # Panics
421+
///
422+
/// If the contract has not yet been initialized.
423+
fn name(env: Env) -> String;
424+
425+
/// Returns the symbol for this token.
426+
///
427+
/// # Panics
428+
///
429+
/// If the contract has not yet been initialized.
430+
fn symbol(env: Env) -> String;
431+
238432
/// Sets the administrator to the specified address `new_admin`.
239433
///
240434
/// # Arguments
@@ -305,13 +499,13 @@ pub trait StellarAssetInterface {
305499
fn clawback(env: Env, from: Address, amount: i128);
306500
}
307501

308-
/// Spec contains the contract spec of Token contracts, including the general
309-
/// interface, as well as the admin interface, such as the Stellar Asset
310-
/// Contract.
502+
/// Spec contains the contract spec of the Stellar Asset Contract.
503+
///
504+
/// The Stellar Asset Contract is a superset of the Token Contract.
311505
#[doc(hidden)]
312506
pub struct StellarAssetSpec;
313507

314-
pub(crate) const SPEC_XDR_INPUT: &[&[u8]] = &[
508+
pub(crate) const STELLAR_ASSET_SPEC_XDR_INPUT: &[&[u8]] = &[
315509
&StellarAssetSpec::spec_xdr_allowance(),
316510
&StellarAssetSpec::spec_xdr_authorized(),
317511
&StellarAssetSpec::spec_xdr_approve(),
@@ -330,14 +524,14 @@ pub(crate) const SPEC_XDR_INPUT: &[&[u8]] = &[
330524
&StellarAssetSpec::spec_xdr_transfer_from(),
331525
];
332526

333-
pub(crate) const SPEC_XDR_LEN: usize = 6456;
527+
pub(crate) const STELLAR_ASSET_SPEC_XDR_LEN: usize = 6456;
334528

335529
impl StellarAssetSpec {
336530
/// Returns the XDR spec for the Token contract.
337-
pub const fn spec_xdr() -> [u8; SPEC_XDR_LEN] {
338-
let input = SPEC_XDR_INPUT;
531+
pub const fn spec_xdr() -> [u8; STELLAR_ASSET_SPEC_XDR_LEN] {
532+
let input = STELLAR_ASSET_SPEC_XDR_INPUT;
339533
// Concatenate all XDR for each item that makes up the token spec.
340-
let mut output = [0u8; SPEC_XDR_LEN];
534+
let mut output = [0u8; STELLAR_ASSET_SPEC_XDR_LEN];
341535
let mut input_i = 0;
342536
let mut output_i = 0;
343537
while input_i < input.len() {

0 commit comments

Comments
 (0)