Test payments are common in crypto. Send a little bit of funds. Wait until the other party has confirmed they received it, then send the whole amount. The reason for this little dance is the fear that the address was somehow miscommunicated, that it’s an old address, a misspelled address, or somebody else’s address. It takes time, and it can’t happen asynchronously.
Instead consider the following protocol. Alice generates a secret claim_code and sends tez to a smart contract, alongside a hash of the claim_code and the recipient’s address. At any point in time, Alice can call the contract to take back those funds, so even if she entered the wrong hash, the funds are not locked.
Bob who’s the recipient gets the claim_code from Alice off band, maybe via email. The code is secret, but it does not need to be super secret.
Bob can claim the funds by calling the contract with the claim code. The contract verifies that the hash of the claim call and his address match the hash of the deposit and releases the funds.
What if Alice used a bad address? No problem, she can claim back the fund. What if Alice used someone else’s address? They won’t know because they don’t know the claim code. Even if the claim code leaks, for a problem to occur, Alice would have had to use the address of someone who’s constantly scanning the chain to see if there are funds they can erroneously claim. Thus it is much better to keep claim_code secret.
I think this could improve payment UX in crypto quite a bit if well integrated in wallets. Here’s a short 100% untested, 100% unaudited implementation to illustrate what I mean. A full implementation would also support FA1.2 and FA2 which require a slightly different albeit very similar mechanism.
type fund_record = {
sender: address;
amount: tez;
}
type storage = (bytes, fund_record) big_map
// Entry point for depositing funds
let deposit (storage: storage) (hash: bytes) : storage =
let record: fund_record = {sender = Tezos.get_sender (); amount = Tezos.get_amount ()} in
if Big_map.mem hash storage then
failwith "Record already exists"
else
Big_map.update hash (Some(record)) storage
// Entry point for claiming funds
let claim (storage: storage) (claim_code: bytes) (destination: address) : (operation list) * storage =
let destination_contract : unit contract = Tezos.get_contract destination in
let hash = Crypto.blake2b (Bytes.pack (claim_code, Tezos.get_sender())) in
match Big_map.find_opt hash storage with
| Some record ->
let op = Tezos.transaction () record.amount destination_contract in
([op], Big_map.remove hash storage)
| _ -> failwith "Invalid record"
// Entry point for refunding funds
let refund (storage: storage) (hash: bytes) (destination: address): (operation list) * storage =
let destination_contract : unit contract = Tezos.get_contract destination in
match Big_map.find_opt hash storage with
| Some record ->
if record.sender = Tezos.get_sender () then
let op = Tezos.transaction () record.amount destination_contract in
([op], Big_map.remove hash storage)
else
failwith "Invalid caller"
| _ -> failwith "Invalid record"
// Main entry point
type action =
| Deposit of bytes
| Claim of bytes * address
| Refund of bytes * address
| Default
let main (param: action) (storage: storage) : (operation list) * storage =
match param with
Deposit (hash) -> ([], deposit storage hash)
| Claim (claim_code, destination) -> claim storage claim_code destination
| Refund (hash, destination) -> refund storage hash destination
| Default -> failwith "Do not send funds to the default entry point"