Skip to content

Instantly share code, notes, and snippets.

@tuncatunc
Last active December 5, 2022 19:34
Show Gist options
  • Save tuncatunc/3cfea15d6fddd29c6c0e1e9f8a683c08 to your computer and use it in GitHub Desktop.
Save tuncatunc/3cfea15d6fddd29c6c0e1e9f8a683c08 to your computer and use it in GitHub Desktop.
const HELP = `Usage example: \n\nnode transfer-create.js k:{public-key} amount -- Replace {public-key} with an actual key`;
import Pact from "pact-lang-api"
const NETWORK_ID = 'development'//'testnet04';
const CHAIN_ID = '0';
const API_HOST = `http://localhost:8080/chainweb/0.0/${NETWORK_ID}/chain/${CHAIN_ID}/pact`;
const KEY_PAIR = {
publicKey: "368820f80c324bbc7c2b0610688a7da43e39f91d118732671cd9c7500ff43cca",
secretKey: "251a920c403ae8c8f65f59142316af3c82b631fba46ddea92ee8c95035bd2898"
};
const creationTime = () => Math.round(new Date().getTime() / 1000);
if (process.argv.length !== 4) {
console.info(HELP);
process.exit(1);
}
if (KEY_PAIR.publicKey === "" || KEY_PAIR.secretKey === "") {
console.error("Please set a key pair");
process.exit(1);
}
// transferCreate(KEY_PAIR.publicKey, process.argv[2], process.argv[3]);
transferCreate("sender00", process.argv[2], process.argv[3]);
async function transferCreate(sender, newAccount, amount) {
const cmd = {
networkId: NETWORK_ID,
keyPairs: [
Object.assign(KEY_PAIR, {
clist: [
Pact.lang.mkCap(
"GAS",
"Capability to allow buying gas",
"coin.GAS",
[]
).cap,
Pact.lang.mkCap(
"Transfer",
"Capability to allow coin transfer",
"coin.TRANSFER",
[sender, newAccount, { decimal: amount }]
).cap
]
})
],
pactCode: `(free.k.transfer-create "${sender}" "${newAccount}" (read-keyset "account-keyset") ${amount})`,
envData: {
"account-keyset": {
keys: [
// Drop the k:
newAccount.substr(2)
],
pred: "keys-all"
}
},
meta: {
creationTime: creationTime(),
ttl: 28000,
gasLimit: 60000,
chainId: CHAIN_ID,
gasPrice: 0.0000001,
// sender: KEY_PAIR.publicKey
sender: "sender00"
}
};
const response = await Pact.fetch.send(cmd, API_HOST);
if (!response.requestKeys)
{
console.error(response)
return
}
console.log(`Request key: ${response.requestKeys[0]}`);
// console.log("Transaction pending...");
const txResult = await Pact.fetch.listen(
{ listen: response.requestKeys[0] },
API_HOST
);
console.log("Transaction mined!");
console.log(JSON.stringify(txResult, null, 2));
}
(namespace "free")
(define-keyset "free.k-admin-keyset"
(read-keyset "k-admin-keyset"))
(module k GOVERNANCE
@doc "K token smart contract"
@model
[ (defproperty conserves-mass (amount:decimal)
(= (column-delta token-table 'balance) 0.0))
(defproperty valid-account-id (accountId:string)
(and
(>= (length accountId) 3)
(<= (length accountId) 256)))
]
(implements fungible-v2)
(implements fungible-xchain-v1)
; --------------------------------------------------------------------------
; Schemas and Tables
(defschema token-schema
@doc " An account, holding a token balance. \
\ \
\ ROW KEY: accountId. "
balance:decimal
guard:guard
)
(deftable token-table:{token-schema})
; --------------------------------------------------------------------------
; Capabilities
(defcap GOVERNANCE
()
@doc " Give the admin full access to call and upgrade the module. "
(enforce-keyset "free.k-admin-keyset")
)
(defcap ACCOUNT_GUARD
( accountId:string )
@doc " Look up the guard for an account, required to debit from that account. "
(enforce-guard (at 'guard (read token-table accountId ['guard])))
)
(defcap DEBIT
( sender:string )
@doc " Capability to perform debiting operations. "
(enforce-guard (at 'guard (read token-table sender ['guard])))
(enforce (!= sender "") "Invalid sender.")
)
(defcap CREDIT
( receiver:string )
@doc " Capability to perform crediting operations. "
(enforce (!= receiver "") "Invalid receiver.")
)
(defcap TRANSFER:bool
( sender:string
receiver:string
amount:decimal )
@doc " Capability to perform transfer between two accounts. "
@managed amount TRANSFER-mgr
(enforce (!= sender receiver) "Sender cannot be the receiver.")
(enforce-unit amount)
(enforce (> amount 0.0) "Transfer amount must be positive.")
(compose-capability (DEBIT sender))
(compose-capability (CREDIT receiver))
)
(defun TRANSFER-mgr:decimal
( managed:decimal
requested:decimal )
(let ((newbal (- managed requested)))
(enforce (>= newbal 0.0)
(format "TRANSFER exceeded for balance {}" [managed]))
newbal
)
)
(defcap TRANSFER_XCHAIN:bool
( sender:string
receiver:string
amount:decimal
target-chain:string
)
@managed amount TRANSFER_XCHAIN-mgr
(enforce-unit amount)
(enforce (> amount 0.0) "Cross-chain transfers require a positive amount")
(compose-capability (DEBIT sender))
)
(defun TRANSFER_XCHAIN-mgr:decimal
( managed:decimal
requested:decimal
)
(enforce (>= managed requested)
(format "TRANSFER_XCHAIN exceeded for balance {}" [managed]))
0.0
)
(defcap TRANSFER_XCHAIN_RECD:bool
( sender:string
receiver:string
amount:decimal
source-chain:string
)
@event true
)
; --------------------------------------------------------------------------
; Constants
(defconst INITIAL_SUPPLY:decimal 1000000000.0
" Initial supply of 1 billion tokens. ")
(defconst DECIMALS 12
" Specifies the minimum denomination for token transactions. ")
(defconst ACCOUNT_ID_CHARSET CHARSET_LATIN1
" Allowed character set for account IDs. ")
(defconst ACCOUNT_ID_PROHIBITED_CHARACTER "$")
(defconst ACCOUNT_ID_MIN_LENGTH 3
" Minimum character length for account IDs. ")
(defconst ACCOUNT_ID_MAX_LENGTH 256
" Maximum character length for account IDs. ")
; --------------------------------------------------------------------------
; Utilities
(defun validate-account-id
( accountId:string )
@doc " Enforce that an account ID meets charset and length requirements. "
(let ((accountLength (length accountId)))
(enforce
(>= accountLength ACCOUNT_ID_MIN_LENGTH)
(format
"Account ID does not conform to the min length requirement: {}"
[accountId]))
(enforce
(<= accountLength ACCOUNT_ID_MAX_LENGTH)
(format
"Account ID does not conform to the max length requirement: {}"
[accountId]))
)
(enforce
(is-charset ACCOUNT_ID_CHARSET accountId)
(format
"Account ID does not conform to the required charset: {}"
[accountId]))
(enforce
(not (contains accountId ACCOUNT_ID_PROHIBITED_CHARACTER))
(format "Account ID contained a prohibited character: {}" [accountId]))
)
;; ; --------------------------------------------------------------------------
;; ; Fungible-v2 Implementation
(defun transfer-create:string
( sender:string
receiver:string
receiver-guard:guard
amount:decimal )
@doc " Transfer to an account, creating it if it does not exist. "
@model [ (property (conserves-mass amount))
(property (> amount 0.0))
(property (valid-account-id sender))
(property (valid-account-id receiver))
(property (!= sender receiver)) ]
(with-capability (TRANSFER sender receiver amount)
(debit sender amount)
(credit receiver receiver-guard amount)
)
)
(defun transfer:string
( sender:string
receiver:string
amount:decimal )
@doc " Transfer to an account, failing if the account does not exist. "
@model [ (property (conserves-mass amount))
(property (> amount 0.0))
(property (valid-account-id sender))
(property (valid-account-id receiver))
(property (!= sender receiver)) ]
(enforce (!= sender receiver)
"sender cannot be the receiver of a transfer")
(validate-account-id sender)
(validate-account-id receiver)
(enforce (> amount 0.0)
"transfer amount must be positive")
(enforce-unit amount)
(with-read token-table receiver
{ "guard" := guard }
(transfer-create sender receiver guard amount)
)
)
(defun debit
( accountId:string
amount:decimal )
@doc " Decrease an account balance. Internal use only. "
@model [ (property (> amount 0.0))
(property (valid-account-id accountId))
]
(validate-account-id accountId)
(enforce (> amount 0.0) "Debit amount must be positive.")
(enforce-unit amount)
(require-capability (DEBIT accountId))
(with-read token-table accountId
{ "balance" := balance }
(enforce (<= amount balance) "Insufficient funds.")
(update token-table accountId
{ "balance" : (- balance amount) }
)
)
)
(defun credit
( accountId:string
guard:guard
amount:decimal )
@doc " Increase an account balance. Internal use only. "
@model [ (property (> amount 0.0))
(property (valid-account-id accountId))
]
(validate-account-id accountId)
(enforce (> amount 0.0) "Credit amount must be positive.")
(enforce-unit amount)
(require-capability (CREDIT accountId))
(with-default-read token-table accountId
{ "balance" : -1.0
, "guard" : guard
}
{ "balance" := balance
, "guard" := currentGuard
}
(enforce (= currentGuard guard) "Account guards do not match.")
(let ((is-new
(if (= balance -1.0)
(enforce-reserved accountId guard)
false)))
(write token-table accountId
{ "balance" : (if is-new amount (+ balance amount))
, "guard" : currentGuard
}
))
)
)
(defun check-reserved:string (account:string)
" Checks ACCOUNT for reserved name and returns type if \
\ found or empty string. Reserved names start with a \
\ single char and colon, e.g. 'c:foo', which would return 'c' as type."
(let ((pfx (take 2 account)))
(if (= ":" (take -1 pfx)) (take 1 pfx) "")))
(defun enforce-reserved:bool (account:string guard:guard)
@doc "Enforce reserved account name protocols."
(if (validate-principal guard account)
true
(let ((r (check-reserved account)))
(if (= r "")
true
(if (= r "k")
(enforce false "Single-key account protocol violation")
(enforce false
(format "Reserved protocol guard violation: {}" [r]))
)))))
(defschema crosschain-schema
@doc " Schema for yielded value in cross-chain transfers "
receiver:string
receiver-guard:guard
amount:decimal
source-chain:string
)
(defpact transfer-crosschain:string
( sender:string
receiver:string
receiver-guard:guard
target-chain:string
amount:decimal )
@model [ (property (> amount 0.0))
(property (!= receiver ""))
(property (valid-account-id sender))
(property (valid-account-id receiver))
]
(step
(with-capability (TRANSFER_XCHAIN sender receiver amount target-chain)
(validate-account-id sender)
(validate-account-id receiver)
(enforce (!= "" target-chain) "empty target-chain")
(enforce (!= (at 'chain-id (chain-data)) target-chain)
"cannot run cross-chain transfers to the same chain")
(enforce (> amount 0.0)
"transfer quantity must be positive")
(enforce-unit amount)
;; Step 1 - debit sender account on current chain
(debit sender amount)
(emit-event (TRANSFER sender "" amount))
(let
((crosschain-details:object{crosschain-schema}
{ "receiver" : receiver
, "receiver-guard" : receiver-guard
, "amount" : amount
, "source-chain" : (at 'chain-id (chain-data))
}
))
(yield crosschain-details target-chain)
)
)
)
(step
(resume
{ "receiver" := receiver
, "receiver-guard" := receiver-guard
, "amount" := amount
}
;; Step 2 - credit receiver account on target chain
(with-capability (CREDIT receiver)
(credit receiver receiver-guard amount)
)
)
)
)
(defun get-balance:decimal
( account:string )
(at 'balance (read token-table account ['balance]))
)
(defun details:object{fungible-v2.account-details}
( account:string )
(with-read token-table account
{ "balance" := balance
, "guard" := guard
}
{ "account" : account
, "balance" : balance
, "guard" : guard
}
)
)
(defun precision:integer
()
DECIMALS
)
(defun enforce-unit:bool
( amount:decimal )
@doc " Enforce the minimum denomination for token transactions. "
(enforce
(= (floor amount DECIMALS) amount)
(format "Amount violates minimum denomination: {}" [amount])
)
)
(defun create-account:string
( account:string
guard:guard )
@doc " Create a new account. "
@model [ (property (valid-account-id account)) ]
(validate-account-id account)
(enforce-reserved account guard)
(insert token-table account
{ "balance" : 0.0
, "guard" : guard
}
)
)
(defun rotate:string
( account:string
new-guard:guard )
(with-read token-table account
{ "guard" := oldGuard }
(enforce-guard oldGuard)
(enforce-guard new-guard)
(update token-table account
{ "guard" : new-guard }
)
)
)
)
; ----------
; INITIALIZATION
; ----------
;
; At this point we've established our smart contract: we entered a namespace,
; defined a keyset, and implemented a module. Now it's time to initialize data.
;
; For a typical smart contract, that simply means creating any tables we defined
; in the contract. However, more complex contracts may perform other steps, such
; as calling functions from the module.
;
; Tables are defined in modules, but they are created after them. This ensures
; that the module can be redefined (ie. upgraded) later without necessarily
; having to re-create the table.
;
; Speaking of: it's a common practice to implement the initialization step as an
; 'if' statement that differentiates between an initial deployment and an
; upgrade. As with our keyset definition at the beginning of the contract, this
; can be done by sending an "upgrade" field with a boolean value as part of the
; transaction data
(if (read-msg "upgrade")
"Upgrade complete"
(create-table token-table))
$ node js/k-transfer-create.js k:sender01 33.3
Request key: jDNrbe_RZ7-1vP28OTuMCJPMQMuA4qEyj4V_uwMjKLs
Transaction mined!
{
"gas": 60000,
"result": {
"status": "failure",
"error": {
"callStack": [],
"type": "EvalError",
"message": "",
"info": ""
}
},
"reqKey": "jDNrbe_RZ7-1vP28OTuMCJPMQMuA4qEyj4V_uwMjKLs",
"logs": "i-jRUqeK6E3Tv0QQblEuaRAbn6kVbRmfw90gM0THNaU",
"events": [
{
"params": [
"sender00",
"k:f89ef46927f506c70b6a58fd322450a936311dc6ac91f4ec3d8ef949608dbf1f",
0.006
],
"name": "TRANSFER",
"module": {
"namespace": null,
"name": "coin"
},
"moduleHash": "rE7DU8jlQL9x_MPYuniZJf5ICBTAEHAIFQCB4blofP4"
}
],
"metaData": {
"blockTime": 1670184678668875,
"prevBlockHash": "2O9Vt3bTGLCHcrdnNQWIRBx5M0yC17PyGU1XIl8mpoY",
"blockHash": "chpXH0bH_cjgO8UIOWgaz7HqEiRmjQludlEWcdD6nHE",
"blockHeight": 6377
},
"continuation": null,
"txId": null
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment