ERC-5169: Client Script URI
ERC-5169 lets a token contract declare a list of client-side script URIs that wallets can execute to provide rich token interactions beyond static metadata. The wallet fetches the script (typically JavaScript or TypeScript hosted at IPFS / HTTPS), runs it in a sandboxed iframe, and the script renders custom UI for the token — mint flows, redeem buttons, in-game inventories, configurator forms.
Used by:
- Smart Token Lab's TokenScript — the original use case; tokens carry their own UI.
- Game-asset NFTs — wallets render minigames or character sheets inline.
- Subscription/credential NFTs — embedded "renew" / "verify" buttons.
- Receipt/coupon tokens — embedded redemption flow.
The contract simply exposes a scriptURI() getter returning a list of URIs; the wallet decides which one to execute (typically picks the first one it can fetch).
Required Interface
interface IERC5169 {
event ScriptUpdate(string[] newScriptURI);
function scriptURI() external view returns (string[] memory);
function setScriptURI(string[] memory newScriptURI) external;
}Critically, setScriptURI is issuer-only — letting random callers update the script URI would let an attacker replace the wallet's UI with a phishing form. The standard recommends the contract owner has a multisig or governance gate on this method.
Neo Equivalent: NEP-11 / NEP-17 + scriptURI Getter
The Neo port is a one-method addition to any NEP-11 / NEP-17 contract plus the issuer-controlled setter:
[DisplayName("TokenWithScript")]
public class TokenWithScript : Nep11Token<TokenState>
{
private const byte Prefix_ScriptUri = 0x70;
private const byte Prefix_Owner = 0xff;
[DisplayName("ScriptUpdate")]
public static event Action<string[]> OnScriptUpdate;
public static string[] ScriptURI()
{
var raw = Storage.Get(Storage.CurrentContext, new byte[] { Prefix_ScriptUri });
return raw is null ? new string[0] : (string[])StdLib.Deserialize((ByteString)raw);
}
public static void SetScriptURI(string[] newURIs)
{
var owner = (UInt160)Storage.Get(Storage.CurrentContext, new byte[] { Prefix_Owner });
if (!Runtime.CheckWitness(owner)) throw new Exception("NEP11:NotOwner");
Storage.Put(Storage.CurrentContext, new byte[] { Prefix_ScriptUri }, StdLib.Serialize(newURIs));
OnScriptUpdate(newURIs);
}
}| ERC-5169 (Ethereum) | Neo Equivalent | Notes |
|---|---|---|
scriptURI() returns string[] | ScriptURI() returns string[] | Direct port |
setScriptURI(string[]) issuer-only | SetScriptURI(string[]) owner-witness-checked | |
ScriptUpdate(newScriptURI) event | OnScriptUpdate(newURIs) notification | |
| Wallets fetch + sandbox-execute | Wallets fetch + sandbox-execute | Same off-chain contract |
Wallet Integration
The wallet's role is unchanged from Ethereum: query the contract's ScriptURI() view, fetch the first reachable URI, render the script in a sandbox (iframe with strict CSP), and provide the script with a restricted API to invoke contract methods.
For Neo wallets adopting ERC-5169, the integration is:
// Neo wallet pseudo-code
async function renderToken(contractHash: string, tokenId: string) {
const script = await rpc.invokeFunction(contractHash, 'scriptURI', []);
if (script.state !== 'HALT') return renderDefault(contractHash, tokenId);
const uris = decodeStringArray(script.stack[0]);
for (const uri of uris) {
try {
const code = await fetch(uri).then(r => r.text());
return renderInSandbox(code, { contractHash, tokenId, walletAPI });
} catch { continue; }
}
return renderDefault(contractHash, tokenId);
}The walletAPI exposed to the script must be strictly restricted — it should only let the script propose transactions for the user to approve, never auto-sign anything. The wallet UI shows the user every contract call before execution.
Composition
- ERC-7160 — multi-metadata. Pairs naturally — the script can render different UI per active metadata index.
- ERC-4906 — metadata update event. When the script URI changes, emit ERC-4906 metadata-update so wallets re-fetch.
- ERC-2135 — consumable. Scripts are perfect for redemption flows ("press here to consume").
- ERC-4907 — rental NFT. Scripts can render the rental status, expiry countdown, and unlock UI.
Migration Notes
For Solidity tokens shipping with TokenScript-style UI:
- Replace
scriptURI()Solidity getter withScriptURI()C# method (or compile vianeo-solcfor direct port). - Store the URIs in serialised form (StdLib.Serialize on a
string[]). - Make sure
SetScriptURIis owner-only — phishing risk if anyone can update.
For Neo wallet authors adopting the standard, the security model is the critical part: only invoke methods the user has explicitly approved and never auto-execute transactions on the script's behalf. The script proposes; the user approves.
