ActionCard

An action card triggers actions in the wallet. For example, a transaction, an attestation, a component of a transaction or a signature.

Concept

Action Cards enable wallets and dapp browsers to launch token specific operations. They can be triggered by an external source like a website, but also by a button in the wallet.

Action Cards can be launched by a third party. But they never take place on a website or a server, but exclusively in the wallet. A third party can only start, but not change an action card.

Action Cards enable wallets to add offchain operations to a token and create token specific transactions, which are currently delivered by a website (dApp). For example, if you have deposited a token to a DEFI project and want to redeem or withdraw it, this can be done by clicking a button in the wallet, instead of being dependent on a website to create the transaction for you. At the same time, any website can integrate a TokenScript and offer buttons for token specific operations without needing to know the smart contract.

Action Cards can launch onchain operations like transactions. But they also allow a token creator to add information to his token or integrate offchain operations: The user can sign a message, start a process to attest his email address, send an authorisation to let someone else use your token (if the underlying token's smart contract accepts such authorisations) or create a deal to share (which requires underlying contract's support). Action Cards can also help to transfer an asset to second layer networks like sidechains or offchain networks. They streamline everything what can be done with a token.

Some action cards can only be available in a specific context. For example, if a token represents a soccer game ticket, the action "Show entrance Pass" should only be available at a gate of a stadium.

It is possible to use two action cards at the same time or in combination. This is called a deck. One successful Action Card can enable another, or the combination of two action cards can unlock a new operation.

Code

The XML file

In the XML file, the <ts:action>...</ts:action> tag starts and ends an Action Card. As an easy example, this is how you declare a simple Action Card which adds an "About" page to a token:
 <ts:card type="action">
     <ts:label>
          <ts:string xml:lang="en">About this token</ts:string>
     </ts:label>
     <ts:view xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
         <style type="text/css">&style;</style>
         <script type="text/javascript">&about.en;</script>
     </ts:view>
 </ts:card>

In the XML code you find the reference to a Script, usually written in JavaScript. In this Script you execute the Action Card. As you see, the Script is placed in the "view" tags.

Often you will need more advanced Action Cards, which might involve to use onchain data to create a transaction. A more sophisticated example is an Action Card to renew an ENS domain:
<ts:card type="action" name="renew">
      <ts:label>
          <ts:string xml:lang="en">Renew</ts:string>
      </ts:label>
      <ts:attribute name="renewalPricePerYear">
            <ts:type><ts:syntax>1.3.6.1.4.1.1466.115.121.1.36</ts:syntax></ts:type>
            <ts:label>
                <ts:string xml:lang="en">renewal price per year</ts:string>
            </ts:label>
            <ts:origins>
                <ethereum:call function="rentPrice" contract="ETHRegistrarController" as="uint">
                    <ts:data>
                        <ts:string local-ref="userEnsName"/>
                        <ts:uint256>31556952</ts:uint256>
                    </ts:data>
                </ethereum:call>
            </ts:origins>
      </ts:attribute>
      <ts:transaction>
          <ethereum:transaction function="renew" contract="ETHRegistrarController" as="uint">
              <ts:data>
                  <ts:string local-ref="userEnsName"/>
                  <ts:uint256>31556952</ts:uint256>
              </ts:data>
              <ethereum:value local-ref="renewalPricePerYear"/>
          </ethereum:transaction>
      </ts:transaction>
      <ts:view xml:lang="en">
            <xhtml:style type="text/css">&style;</xhtml:style>
            <xhtml:script type="text/javascript">&renew.en;</xhtml:script>
      </ts:view>
</ts:card>

In this example a few things happen: In the <ts:attribute> part you declare Attributes you will later use in the JavaScript file. With an Ethereum call you learn the renewal price per year - an important information to build a transaction. With the <ts:transaction> tag you create the transaction to renew the ENS do▷The JavaScript file◁main. Finally, inside the view tags, you link to a renew.en.js JavaScript file.

The JavaScript file

The operations of an Action Card are written in an JavaScript file. All JavaScript files must be declared at the top of the xml file (as well as css files and images you use):
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE token  [
        <!ENTITY style SYSTEM "shared.css">
        <!ENTITY about.en SYSTEM "about.en.js">
        <!ENTITY approve.en SYSTEM "approve.en.js">
        ]>
Here is a simple example for a very simple Action Card displaying an about page in the wallet.
class Token {
    constructor(tokenInstance) {
        this.props = tokenInstance;
        this.setConfirm();
    }
    setConfirm() {
        window.onConfirm = function() {
            window.close();
        }
    }
    render() {
        return`
        <div class="ui container">
          <div class="ui segment">
            <span><bold><h1>This is my token!</span>
          </div>
        </div>
`;
    }
}
web3.tokens.dataChanged = (oldTokens, updatedTokens, tokenIdCard) => {
    const currentTokenInstance = web3.tokens.data.currentInstance;
    document.getElementById(tokenIdCard).innerHTML = new Token(currentTokenInstance).render();
};

The file starts with declaring a Token class. Here you can define properties of the token and import attributes. In the "render()" part you can put your individual message in html. In this case it is simply the message "This is my token!". But you can also add images from websites, API calls and much more.

For a more complicated example, we turn to the ENS TokenScript again and show how to it renews an ENS domain. It is important to note that the renewal transaction is not created in the JavaScript file, but on the XML layer. The JavaScript file just renders the user interface, so that the user exactly knows what he is doing. It could also be used to import information from a third party, process a QR code (like scanning a ticket) and much more.

class Token {

    //TODO add slider to determine the duration by year
    constructor(tokenInstance) {
        this.props = tokenInstance;
        this.props.baseNode = ".eth";
        this.props.fullName = this.props.ensName + this.props.baseNode;
        if(this.props.fullName === ".eth" || this.props.fullName === "undefined.eth") {
            // if ENS name cannot be retrieved by events because it is legacy, use the opensea format
            this.props.fullName = this.props.name;
            //remove .eth
            this.props.ensName = this.props.name.replace(this.props.baseNode, "");
        }
        web3.action.setProps({
            userEnsName: this.props.ensName
        });
    }

    formatTimeStamp(UNIX_timestamp) {
        let a = new Date(UNIX_timestamp * 1000);
        let months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
        let year = a.getFullYear();
        let month = months[a.getMonth()];
        let date = a.getDate();
        return date + ' ' + month + ' ' + year;
    }

    render() {
        return`
        <div class="ui container">
          <div class="ui segment">
              <img src="data:image/png;base64,i...C">
              <span><bold><h3>Renew <strong>${this.props.fullName}</strong> for one year</h3></bold></span>
              <div id="inputBox">
                 <h3>Price of one year renewal: ${(this.props.renewalPricePerYear / 1e+18).toFixed(3)} ETH</h3>
                 <h3>Name expires: ${this.formatTimeStamp(this.props.nameExpires)}</h3>
                 <br>
              </div>
          </div>
        </div>`;
    }
}

web3.tokens.dataChanged = (oldTokens, updatedTokens, tokenCardId) => {
    const currentTokenInstance = updatedTokens.currentInstance;
    let token = new Token(currentTokenInstance);
    document.getElementById(tokenCardId).innerHTML = token.render();
};