-
Notifications
You must be signed in to change notification settings - Fork 14
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
More integration tests #103
Conversation
9c41e14
to
39fc914
Compare
965e280
to
99f29a7
Compare
d0edd2f
to
6ec08a3
Compare
Doing this can unintentionally revert to the pre-hook state if the receiver hook carried out further operations on the balance received
…erywhere can only link against one actor for shared structs, because of the invoke methods using un-mangled names (causing collisions at link time if more than one is included)
original recipient just keeps the tokens in this case, instead of rejecting the transfer they originally received
…dling as Mint does
the caller can check for success or failure of the transfer and also has the TransferReturn data available for use
6ec08a3
to
c494ff9
Compare
testing/fil_token_integration/actors/basic_token_actor/src/lib.rs
Outdated
Show resolved
Hide resolved
amount: token_params.amount, | ||
}; | ||
let receipt = sdk::send::send(&Address::new_id(sdk::message::caller()), method_hash!("Burn"), RawBytes::serialize(&burn_params).unwrap(), TokenAmount::zero()).unwrap(); | ||
if !receipt.exit_code.is_success() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure if this should explode or just return the result upstream (so failure to burn will keep the tokens) like Transfer does above
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would say it should abort and so should transfer, but I see you're using this return value in tests, so ok to keep this behaviour if it's useful.
amount: balance, | ||
}; | ||
let receipt = sdk::send::send(¶ms.token_address, method_hash!("Burn"), RawBytes::serialize(&burn_params).unwrap(), TokenAmount::zero()).unwrap(); | ||
if !receipt.exit_code.is_success() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as the Receive hook version, explode here or fail quietly and let the caller figure it out?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I reckon fail for both
to_balance: self.balance_of(to)?, | ||
recipient_data: ret.recipient_data, | ||
} | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seeing this, I'm now wondering about yet another change to the hook return to encourage safety. A token impl should generally re-inspect state to create the return value. There's a possible optimisation where the CID hasn't changed, but the general case is to reload.
So now I think we should not return the ret from the hook, but force the caller to recreate it. But make that easy by adding token methods that create a TransferReturn, MintReturn etc from the current state. The hook return would only contain the recipient data.
So, sketching...
let hook_ret = hook.call()?;
self.reload()?; // reloads state only if CID has changed
return self.util.transfer_return(hook_ret);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think things get simpler with an approach like that. Might want to rename the underlying functions from mint
to mint_init
or something to make clearer that it's a multi-step process? I'll leave that alone for the moment but it's a quick enough change afterwards if we want it in there.
The reload
in your example probably needs to take the original CID we got when flushing the state before the hook call, but this otherwise looks like a simple enough change. Will try it out once I've addressed other review feedback as it's a more complex change than those.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Had a go at it, definitely feels nicer to use when implementing a token.
Transfer(Address, RawBytes), | ||
/// Burn incoming tokens | ||
Burn, | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice
amount: balance, | ||
}; | ||
let receipt = sdk::send::send(¶ms.token_address, method_hash!("Burn"), RawBytes::serialize(&burn_params).unwrap(), TokenAmount::zero()).unwrap(); | ||
if !receipt.exit_code.is_success() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I reckon fail for both
assert!(ret_val.msg_receipt.exit_code.is_success()); | ||
// check the receipt we got in return data | ||
let receipt = ret_val.msg_receipt.return_data.deserialize::<Receipt>().unwrap(); | ||
assert!(!receipt.exit_code.is_success()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Critical to assert all the balances here, and after every transfer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Think I've covered all these now.
Also added some extra checks of TransferReturn
data in a couple of the more complex cases. I want to do more of that in a follow-up along with the remaining few test cases. The multi-layered stuff gets messy real quick because each struct serializes the next layer in its return_data
or recipient_data
. I want to spend some time coming up with a nicer way to handle that stuff but I figure it's a low priority for the moment.
cc59756
to
2acef81
Compare
…educe test case size
…Return afterwards
/// Replace the current state with another | ||
/// The previous state is returned and can be safely dropped | ||
pub fn replace(&mut self, state: TokenState) -> TokenState { | ||
std::mem::replace(self.state, state) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this is what I want. I think I was confused when I asked for it.
What I need would be be parallel with wrap
, taking a mutable reference. I have a new State
(embedded in another struct) that I've loaded into memory, and now want a token wrapping it. I want future mutations to mutate that reference in place (so I can persist later). I don't want to further mutate the state behind the old reference.
Perhaps I could just wrap
again to get a new token in this case, but a re-wrap(&mut new_state)
would still be convenient.
We should probably restore the old replace
method for its use in testing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What you're describing might be better done with std::mem::swap
. Though it may also be better to just treat the Token
as disposable and create a new one as needed. Which I guess is what we'd end up doing in a re-wrap function - create a new Token by taking everything but the state from the old one.
This new replace
doesn't hurt the tests, it's a better version of what they were using before.
let supply = state.change_supply_by(amount)?; | ||
Ok(MintReturn { balance, supply: supply.clone(), recipient_data: RawBytes::default() }) | ||
state.change_balance_by(&bs, owner_id, amount)?; | ||
state.change_supply_by(amount)?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Discussion: in most cases, when there is no recursion, the value returned here will be the right one. It would be nice to avoid traversing the balance table again to look up the value when that happens. So storing it in the intermediate for that case could help. That then raises the question of flowing information through to the token so that it knows whether it needs to look up a new balance. Maybe an internal dirty flag or mutation count?
I think this is a nuanced enough issue that you should write an issue and defer it to a new PR. Let's get it correct now, and then optimise.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Made issue #110 for it
#[derive(Debug)] | ||
pub struct MintIntermediate { | ||
/// Recipient address to use for querying balance | ||
pub recipient: Address, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we restrict visibility of these to the crate? I'm undecided, so ok to leave public for now.
More tests that build on #95
Adds a configurable test actor for testing different scenarios such as those outlined in #80
Single actor tests implemented:
Multi-actor tests implemented:
Cases from #80 still not implemented:
Tests are combined into two files (single- and multi-actor) as a compromise between readability and CI build/run time.
There's also a mechanism to reload the current state from a
Cid
which is used inMint
,Transfer
andTransferFrom
to update state and then the return value if the state has changed since calling the receiver hook. This covers cases such as transferring or burning from within the receiver hook. It could also be used to revert state if the hook call aborts, which may be useful to some implementations?