Sending assets with time lock

Hi,
Does anyone know if there is a wallet that currently support to send a transaction where the assets you send are time locked?

I want to send an NFT to a community member but with the condition that he/she cannot sell or give it away for a certain set of time.

Thanks!
//Appelcore

That cannot be completely achieved. If you send to their address directly, they will always have complete control immediately.

You could send it to a contract that is time-locked. A simple script would be sufficient for that and there is no need for Plutus. But then they would have to actively move it from the contract address to their own wallet once the time lock expires. It probably won’t show up in their wallet until then.

And constructing the transaction that moves from the time-locked script address to their own wallet is not trivial and not really supported by wallet apps. So, you would want to provide a dApp website for doing that.

Such a contract address can use the stake key of the user immediately. So, for dApps that decide what is owned by the user based on the stake part of the address, it will look like the user owns that NFT immediately. You’d have to test which wallet apps and dApps already consider it to be in the wallet and show it even if it is on a contract address and cannot be controlled/moved by the wallet.

Thank you for taking your time to answer.

It wasn’t what I had hoped for but what I expected. Is there another service you know of that can solve this issue. Like a future promise to send an asset?

I’d say a time-locked simple script comes close.

Say, I want to send something to my address addr1qyh72hvvrurjvddx4gq37jd2fzyef8scz9cwcyc90dffq0xxllh3nc5r82ujj36fy9zh0gryqvqy7r3ejd2h2kgsvryswhjr9q locked until slot 108 900 000 (some time tomorrow, converting times to slots and vice versa is a different topic).

I can decode that address with:

$ echo addr1qyh72hvvrurjvddx4gq37jd2fzyef8scz9cwcyc90dffq0xxllh3nc5r82ujj36fy9zh0gryqvqy7r3ejd2h2kgsvryswhjr9q | bech32
012fe55d8c1f072635a6aa011f49aa4889949e181170ec13057b52903cc6ffef19e2833ab9294749214577a06403004f0e39935575591060c9

The first byte 01 means that it is a delegated base address – payment key hash and stake key hash – the payment key hash are the next 28 bytes:

$ echo addr1qyh72hvvrurjvddx4gq37jd2fzyef8scz9cwcyc90dffq0xxllh3nc5r82ujj36fy9zh0gryqvqy7r3ejd2h2kgsvryswhjr9q | bech32 | cut -c 3-58
2fe55d8c1f072635a6aa011f49aa4889949e181170ec13057b52903c

And the stake key hash are the final 28 bytes:

$ echo addr1qyh72hvvrurjvddx4gq37jd2fzyef8scz9cwcyc90dffq0xxllh3nc5r82ujj36fy9zh0gryqvqy7r3ejd2h2kgsvryswhjr9q | bech32 | cut -c 59-114
c6ffef19e2833ab9294749214577a06403004f0e39935575591060c9

The lock script would be:

{ "type": "all",
  "scripts": [ { "type": "sig",
                 "keyHash": "2fe55d8c1f072635a6aa011f49aa4889949e181170ec13057b52903c" },
               { "type": "after",
                 "slot": 108900000 } ] }

Meaning that to fulfil this script, a transaction has to be signed by the private payment key for the public key hash of my address and the slot has to be greater than 108 900 000.

The address to send to can then be generated by cardano-cli (with that lock script in lock.json):

$ cardano-cli address build --mainnet --payment-script-file lock.json --stake-address stake1u8r0lmceu2pn4wffgayjz3th5pjqxqz0pcuex4t4tygxpjgygq2ay
addr1z9tcswppsxrgyty78yyf0823886zt9y8jpexftgnm47etgxxllh3nc5r82ujj36fy9zh0gryqvqy7r3ejd2h2kgsvrys3cyt3x

If I send something to that address, it is staked with the rest of my stake and can only be moved after some time tomorrow with a signature from my payment key.

The stake address (that I just know because it’s my account) can be derived from the stake part extracted from the address:

$ echo e1c6ffef19e2833ab9294749214577a06403004f0e39935575591060c9 | bech32 stake
stake1u8r0lmceu2pn4wffgayjz3th5pjqxqz0pcuex4t4tygxpjgygq2ay

(The prefix e1 says that it is a stake key hash.)

Most Cardano libraries have functionality for all this that can be used instead of using bech32 and cardano-cli on the command line. Consult the documentation.

2 Likes

For what it’s worth, I have just sent one of my self-created NFTs to that address. Watch me tomorrow when I create the transaction to move it back.

https://adastat.net/addresses/addr1z9tcswppsxrgyty78yyf0823886zt9y8jpexftgnm47etgxxllh3nc5r82ujj36fy9zh0gryqvqy7r3ejd2h2kgsvrys3cyt3x

On Cardanoscan, I could also verify the script belonging to that address:
https://cardanoscan.io/address/addr1z9tcswppsxrgyty78yyf0823886zt9y8jpexftgnm47etgxxllh3nc5r82ujj36fy9zh0gryqvqy7r3ejd2h2kgsvrys3cyt3x?tab=script

Eternl shows that as:


…, but the NFT is not shown in my list of tokens (which is kind of correct since I would expect that I can do anything I want with the tokens in that list which I cannot do with this one).

Pool.pm on the other hand considers stake keys as owners and shows me as the owner:
https://pool.pm/asset10zsrgd7329r73aq7ms3hvv0k2c727q55uszdfc

I will have to try this :pray:
Great answer btw. Truly appriciated

So, that thing is unlocked now and we can spend it.

I’m going to show how to construct the transaction with cardano-cli on the command line. If you want to provide a dApp for users to claim the unlocked token, you’d need to do the equivalent in the library of your choice.

$ cardano-cli transaction build --mainnet \
> --tx-in 76bf9ce4f5685856d86ba211db9712681ebd30af0b17f82bef1adeaa970d25fe#0 \
> --tx-in-script-file lock.json \
> --required-signer-hash 2fe55d8c1f072635a6aa011f49aa4889949e181170ec13057b52903c \
> --invalid-before 108900000 \
> --change-address addr1qyh72hvvrurjvddx4gq37jd2fzyef8scz9cwcyc90dffq0xxllh3nc5r82ujj36fy9zh0gryqvqy7r3ejd2h2kgsvryswhjr9q \
> --out-file claim-tx.json
Estimated transaction fee: Lovelace 170781

The --tx-in is the UTxO where the NFT came to be above. You would have to save that information somewhere in your backend.

The --tx-in-script-file is the same as above. It has to be provided in the transaction since the address only contains the hash of the script. The script is not known on-chain by now.

The --required-signer-hash tells cardano-cli to include the requirement for the signature in the transaction without the need to evaluate the script itself.

The --invalid-before ensures that the transaction can only be submitted if the time lock in the script is fulfilled. (This has to be given explicitly. The script requires a matching --invalid-before to be there instead of being evaluated itself at block production time.)

The --change-address tells cardano-cli to just put everything on that address (my receive address already seen above).

Since I’m using a Ledger, I’m witnessing/signing this with cardano-hw-cli and then assembling the transaction with the witness:

$ cardano-hw-cli transaction witness \
> --tx-file claim-tx.json \
> --hw-signing-file account1.hwsfile \
> --out-file claim-witness.json
$ cardano-cli transaction assemble \
> --tx-body-file claim-tx.json \
> --witness-file claim-witness.json \
> --out-file claim-tx-signed.json

For a software wallet, you would use the .skey file to witness/sign it. That can also be derived from a seed phrase with cardano-address.
For a dApp, the equivalent step is to give the unsigned transaction to the CIP-30 connector’s signTx() method.

Finally, the signed transaction can be submitted:

$ cardano-cli-mainnet transaction submit --mainnet \
> --tx-file claim-tx-signed.json
Transaction successfully submitted.

And, in fact, the claim transaction appears a few seconds later in Eternl:


And on Adastat:
https://adastat.net/transactions/5406c370c36a5a793368000229f846603766f0800afc44c1c45274a9f719d91c

1 Like