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

Include the token interface in the stellar asset interface #1427

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 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
18 changes: 10 additions & 8 deletions soroban-sdk-macros/src/derive_spec_fn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,20 +156,22 @@ pub fn derive_fn_spec(
return Err(quote! { #(#compile_errors)* });
}

let export_attr = if export {
Some(quote! { #[cfg_attr(target_family = "wasm", link_section = "contractspecv0")] })
let exported = if export {
Some(quote! {
#[doc(hidden)]
#[allow(non_snake_case)]
#[allow(non_upper_case_globals)]
#(#attrs)*
#[cfg_attr(target_family = "wasm", link_section = "contractspecv0")]
pub static #spec_ident: [u8; #spec_xdr_len] = #ty::#spec_fn_ident();
})
} else {
None
};

// Generated code.
Ok(quote! {
#[doc(hidden)]
#[allow(non_snake_case)]
#[allow(non_upper_case_globals)]
#(#attrs)*
#export_attr
pub static #spec_ident: [u8; #spec_xdr_len] = #ty::#spec_fn_ident();
#exported

impl #ty {
#[allow(non_snake_case)]
Expand Down
55 changes: 51 additions & 4 deletions soroban-sdk/src/tests/token_spec.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,29 @@
use crate as soroban_sdk;
use std::collections::HashSet;

use soroban_sdk::{
token::{StellarAssetSpec, SPEC_XDR_INPUT, SPEC_XDR_LEN},
token::{
StellarAssetSpec, TokenSpec, STELLAR_ASSET_SPEC_XDR_INPUT, STELLAR_ASSET_SPEC_XDR_LEN,
TOKEN_SPEC_XDR_INPUT, TOKEN_SPEC_XDR_LEN,
},
xdr::{Error, Limited, Limits, ReadXdr, ScSpecEntry},
};

extern crate std;
#[test]
fn test_stellar_asset_spec_xdr_len() {
let len = STELLAR_ASSET_SPEC_XDR_INPUT
.iter()
.fold(0usize, |sum, x| sum + x.len());
assert_eq!(STELLAR_ASSET_SPEC_XDR_LEN, len);
}

#[test]
fn test_spec_xdr_len() {
let len = SPEC_XDR_INPUT.iter().fold(0usize, |sum, x| sum + x.len());
assert_eq!(SPEC_XDR_LEN, len);
fn test_token_spec_xdr_len() {
let len = TOKEN_SPEC_XDR_INPUT
.iter()
.fold(0usize, |sum, x| sum + x.len());
assert_eq!(TOKEN_SPEC_XDR_LEN, len);
}

#[test]
Expand All @@ -22,3 +35,37 @@ fn test_spec_xdr() -> Result<(), Error> {
}
Ok(())
}

#[test]
fn test_token_spec_xdr() -> Result<(), Error> {
let xdr = TokenSpec::spec_xdr();
let cursor = std::io::Cursor::new(xdr);
for spec_entry in ScSpecEntry::read_xdr_iter(&mut Limited::new(cursor, Limits::none())) {
spec_entry?;
}
Ok(())
}

#[test]
fn test_stellar_asset_spec_includes_token_spec() -> Result<(), Error> {
// Read all TokenSpec entries
let token_xdr = TokenSpec::spec_xdr();
let token_cursor = std::io::Cursor::new(token_xdr);
let token_entries: HashSet<ScSpecEntry> =
ScSpecEntry::read_xdr_iter(&mut Limited::new(token_cursor, Limits::none()))
.collect::<Result<HashSet<_>, _>>()?;

// Read all StellarAssetSpec entries
let stellar_asset_xdr = StellarAssetSpec::spec_xdr();
let stellar_asset_cursor = std::io::Cursor::new(stellar_asset_xdr);
let stellar_asset_entries: HashSet<ScSpecEntry> =
ScSpecEntry::read_xdr_iter(&mut Limited::new(stellar_asset_cursor, Limits::none()))
.collect::<Result<HashSet<_>, _>>()?;

// Check that the token entries are a subset of stellar entries
assert!(
token_entries.is_subset(&stellar_asset_entries),
"StellarAssetSpec is missing entries from TokenSpec"
);
Ok(())
}
212 changes: 203 additions & 9 deletions soroban-sdk/src/token.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ pub use TokenClient as Client;
/// There are no functions in the token interface for minting tokens. Minting is
/// an administrative function that can differ significantly from one token to
/// the next.
#[contractspecfn(name = "StellarAssetSpec", export = false)]
#[contractspecfn(name = "TokenSpec", export = false)]
dmkozh marked this conversation as resolved.
Show resolved Hide resolved
#[contractclient(crate_path = "crate", name = "TokenClient")]
pub trait TokenInterface {
/// Returns the allowance for `spender` to transfer from `from`.
Expand Down Expand Up @@ -230,11 +230,205 @@ pub trait TokenInterface {
fn symbol(env: Env) -> String;
}

/// Spec contains the contract spec of Token contracts.
#[doc(hidden)]
pub struct TokenSpec;

pub(crate) const TOKEN_SPEC_XDR_INPUT: &[&[u8]] = &[
&TokenSpec::spec_xdr_allowance(),
&TokenSpec::spec_xdr_approve(),
&TokenSpec::spec_xdr_balance(),
&TokenSpec::spec_xdr_burn(),
&TokenSpec::spec_xdr_burn_from(),
&TokenSpec::spec_xdr_decimals(),
&TokenSpec::spec_xdr_name(),
&TokenSpec::spec_xdr_symbol(),
&TokenSpec::spec_xdr_transfer(),
&TokenSpec::spec_xdr_transfer_from(),
];

pub(crate) const TOKEN_SPEC_XDR_LEN: usize = 4716;

impl TokenSpec {
/// Returns the XDR spec for the Token contract.
pub const fn spec_xdr() -> [u8; TOKEN_SPEC_XDR_LEN] {
let input = TOKEN_SPEC_XDR_INPUT;
// Concatenate all XDR for each item that makes up the token spec.
let mut output = [0u8; TOKEN_SPEC_XDR_LEN];
let mut input_i = 0;
let mut output_i = 0;
while input_i < input.len() {
let subinput = input[input_i];
let mut subinput_i = 0;
while subinput_i < subinput.len() {
output[output_i] = subinput[subinput_i];
output_i += 1;
subinput_i += 1;
}
input_i += 1;
}

// Check that the numbers of bytes written is equal to the number of bytes
// expected in the output.
if output_i != output.len() {
panic!("unexpected output length",);
}

output
}
}

/// Interface for admin capabilities for Token contracts, such as the Stellar
/// Asset Contract.
#[contractspecfn(name = "StellarAssetSpec", export = false)]
#[contractclient(crate_path = "crate", name = "StellarAssetClient")]
pub trait StellarAssetInterface {
/// Returns the allowance for `spender` to transfer from `from`.
///
/// The amount returned is the amount that spender is allowed to transfer
/// out of from's balance. When the spender transfers amounts, the allowance
/// will be reduced by the amount transferred.
///
/// # Arguments
///
/// * `from` - The address holding the balance of tokens to be drawn from.
/// * `spender` - The address spending the tokens held by `from`.
fn allowance(env: Env, from: Address, spender: Address) -> i128;

/// Set the allowance by `amount` for `spender` to transfer/burn from
/// `from`.
///
/// The amount set is the amount that spender is approved to transfer out of
/// from's balance. The spender will be allowed to transfer amounts, and
/// when an amount is transferred the allowance will be reduced by the
/// amount transferred.
///
/// # Arguments
///
/// * `from` - The address holding the balance of tokens to be drawn from.
/// * `spender` - The address being authorized to spend the tokens held by
/// `from`.
/// * `amount` - The tokens to be made available to `spender`.
/// * `expiration_ledger` - The ledger number where this allowance expires. Cannot
/// be less than the current ledger number unless the amount is being set to 0.
/// An expired entry (where expiration_ledger < the current ledger number)
/// should be treated as a 0 amount allowance.
///
/// # Events
///
/// Emits an event with topics `["approve", from: Address,
/// spender: Address], data = [amount: i128, expiration_ledger: u32]`
fn approve(env: Env, from: Address, spender: Address, amount: i128, expiration_ledger: u32);

/// Returns the balance of `id`.
///
/// # Arguments
///
/// * `id` - The address for which a balance is being queried. If the
/// address has no existing balance, returns 0.
fn balance(env: Env, id: Address) -> i128;

/// Transfer `amount` from `from` to `to`.
///
/// # Arguments
///
/// * `from` - The address holding the balance of tokens which will be
/// withdrawn from.
/// * `to` - The address which will receive the transferred tokens.
/// * `amount` - The amount of tokens to be transferred.
///
/// # Events
///
/// Emits an event with topics `["transfer", from: Address, to: Address],
/// data = amount: i128`
fn transfer(env: Env, from: Address, to: Address, amount: i128);

/// Transfer `amount` from `from` to `to`, consuming the allowance that
/// `spender` has on `from`'s balance. Authorized by spender
/// (`spender.require_auth()`).
///
/// The spender will be allowed to transfer the amount from from's balance
/// if the amount is less than or equal to the allowance that the spender
/// has on the from's balance. The spender's allowance on from's balance
/// will be reduced by the amount.
///
/// # Arguments
///
/// * `spender` - The address authorizing the transfer, and having its
/// allowance consumed during the transfer.
/// * `from` - The address holding the balance of tokens which will be
/// withdrawn from.
/// * `to` - The address which will receive the transferred tokens.
/// * `amount` - The amount of tokens to be transferred.
///
/// # Events
///
/// Emits an event with topics `["transfer", from: Address, to: Address],
/// data = amount: i128`
fn transfer_from(env: Env, spender: Address, from: Address, to: Address, amount: i128);

/// Burn `amount` from `from`.
///
/// Reduces from's balance by the amount, without transferring the balance
/// to another holder's balance.
///
/// # Arguments
///
/// * `from` - The address holding the balance of tokens which will be
/// burned from.
/// * `amount` - The amount of tokens to be burned.
///
/// # Events
///
/// Emits an event with topics `["burn", from: Address], data = amount:
/// i128`
fn burn(env: Env, from: Address, amount: i128);

/// Burn `amount` from `from`, consuming the allowance of `spender`.
///
/// Reduces from's balance by the amount, without transferring the balance
/// to another holder's balance.
///
/// The spender will be allowed to burn the amount from from's balance, if
/// the amount is less than or equal to the allowance that the spender has
/// on the from's balance. The spender's allowance on from's balance will be
/// reduced by the amount.
///
/// # Arguments
///
/// * `spender` - The address authorizing the burn, and having its allowance
/// consumed during the burn.
/// * `from` - The address holding the balance of tokens which will be
/// burned from.
/// * `amount` - The amount of tokens to be burned.
///
/// # Events
///
/// Emits an event with topics `["burn", from: Address], data = amount:
/// i128`
fn burn_from(env: Env, spender: Address, from: Address, amount: i128);

/// Returns the number of decimals used to represent amounts of this token.
///
/// # Panics
///
/// If the contract has not yet been initialized.
fn decimals(env: Env) -> u32;

/// Returns the name for this token.
///
/// # Panics
///
/// If the contract has not yet been initialized.
fn name(env: Env) -> String;

/// Returns the symbol for this token.
///
/// # Panics
///
/// If the contract has not yet been initialized.
fn symbol(env: Env) -> String;

/// Sets the administrator to the specified address `new_admin`.
///
/// # Arguments
Expand Down Expand Up @@ -305,13 +499,13 @@ pub trait StellarAssetInterface {
fn clawback(env: Env, from: Address, amount: i128);
}

/// Spec contains the contract spec of Token contracts, including the general
/// interface, as well as the admin interface, such as the Stellar Asset
/// Contract.
/// Spec contains the contract spec of the Stellar Asset Contract.
///
/// The Stellar Asset Contract is a superset of the Token Contract.
#[doc(hidden)]
pub struct StellarAssetSpec;

pub(crate) const SPEC_XDR_INPUT: &[&[u8]] = &[
pub(crate) const STELLAR_ASSET_SPEC_XDR_INPUT: &[&[u8]] = &[
&StellarAssetSpec::spec_xdr_allowance(),
&StellarAssetSpec::spec_xdr_authorized(),
&StellarAssetSpec::spec_xdr_approve(),
Expand All @@ -330,14 +524,14 @@ pub(crate) const SPEC_XDR_INPUT: &[&[u8]] = &[
&StellarAssetSpec::spec_xdr_transfer_from(),
];

pub(crate) const SPEC_XDR_LEN: usize = 6456;
pub(crate) const STELLAR_ASSET_SPEC_XDR_LEN: usize = 6456;

impl StellarAssetSpec {
/// Returns the XDR spec for the Token contract.
pub const fn spec_xdr() -> [u8; SPEC_XDR_LEN] {
let input = SPEC_XDR_INPUT;
pub const fn spec_xdr() -> [u8; STELLAR_ASSET_SPEC_XDR_LEN] {
let input = STELLAR_ASSET_SPEC_XDR_INPUT;
// Concatenate all XDR for each item that makes up the token spec.
let mut output = [0u8; SPEC_XDR_LEN];
let mut output = [0u8; STELLAR_ASSET_SPEC_XDR_LEN];
let mut input_i = 0;
let mut output_i = 0;
while input_i < input.len() {
Expand Down
Loading