├── README.md ├── data ├── contract │ └── counter.cc ├── evidence │ └── demo.json ├── keys │ ├── address │ ├── private.key │ └── public.key └── wasm │ └── counter.wasm ├── demo.py ├── images └── wallet.png ├── setup.py ├── wallet.py └── xuper ├── __init__.py └── client.py /README.md: -------------------------------------------------------------------------------- 1 | # pythonsdk 2 | 3 | # requirements 4 | python2.7 python3.x 5 | 6 | ecdsa 7 | requests 8 | 9 | # quick start 10 | pip install xuper 11 | 12 | ## start server 13 | 1. 启动xchain 14 | nohup ./xchain & 15 | 2. 启动http网关 16 | nohup ./xchain-httpgw & 17 | 18 | ## A wallet demo on python sdk 19 | ![WalletDemo](https://github.com/xuperchain/pythonsdk/blob/master/images/wallet.png) 20 | 21 | ## Demo of python SDK 22 | ``` 23 | pysdk = xuper.XuperSDK("http://localhost:8098", "xuper") 24 | pysdk.readkeys("./data/keys") 25 | 26 | #1. 普通转账 27 | pysdk.transfer("bob", 88888, desc="hello world") 28 | 29 | #2. 创建合约账号 30 | new_account_name = pysdk.new_account() 31 | print("wait acl confirmed....") 32 | time.sleep(3) 33 | 34 | #3. 部署合约 35 | pysdk.transfer(new_account_name, 10000000, desc="start funds") 36 | pysdk.set_account(new_account_name) 37 | contract_name = 'counter'+str(random.randint(100,1000000)) 38 | print("deploying......") 39 | rsps = pysdk.deploy(new_account_name, contract_name, open('./data/wasm/counter.wasm','rb').read(), {'creator':b'baidu'}) 40 | print(rsps) 41 | 42 | #4. 预执行合约 43 | rsps = pysdk.preexec(contract_name, "get", {"key":b"counter"}) 44 | print(rsps.decode()) 45 | 46 | #5. 调用合约并生效上链 47 | for i in range(5): 48 | rsps = pysdk.invoke(contract_name, "increase", {"key":b"counter"}) 49 | print(rsps) 50 | 51 | ``` 52 | -------------------------------------------------------------------------------- /data/contract/counter.cc: -------------------------------------------------------------------------------- 1 | #include "xchain/xchain.h" 2 | 3 | struct Counter : public xchain::Contract {}; 4 | 5 | DEFINE_METHOD(Counter, initialize) { 6 | xchain::Context* ctx = self.context(); 7 | const std::string& creator = ctx->arg("creator"); 8 | if (creator.empty()) { 9 | ctx->error("missing creator"); 10 | return; 11 | } 12 | ctx->put_object("creator", creator); 13 | ctx->ok("initialize succeed"); 14 | } 15 | 16 | DEFINE_METHOD(Counter, increase) { 17 | xchain::Context* ctx = self.context(); 18 | const std::string& key = ctx->arg("key"); 19 | std::string value; 20 | ctx->get_object(key, &value); 21 | int cnt = 0; 22 | cnt = atoi(value.c_str()); 23 | char buf[32]; 24 | snprintf(buf, 32, "%d", cnt + 1); 25 | ctx->put_object(key, buf); 26 | ctx->ok(buf); 27 | } 28 | 29 | DEFINE_METHOD(Counter, get) { 30 | xchain::Context* ctx = self.context(); 31 | const std::string& key = ctx->arg("key"); 32 | std::string value; 33 | if (ctx->get_object(key, &value)) { 34 | ctx->ok(value); 35 | } else { 36 | ctx->error("key not found"); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /data/evidence/demo.json: -------------------------------------------------------------------------------- 1 | { 2 | "module": "xkernel" 3 | } 4 | -------------------------------------------------------------------------------- /data/keys/address: -------------------------------------------------------------------------------- 1 | dpzuVdosQrF2kmzumhVeFQZa1aYcdgFpN -------------------------------------------------------------------------------- /data/keys/private.key: -------------------------------------------------------------------------------- 1 | {"Curvname":"P-256","X":74695617477160058757747208220371236837474210247114418775262229497812962582435,"Y":51348715319124770392993866417088542497927816017012182211244120852620959209571,"D":29079635126530934056640915735344231956621504557963207107451663058887647996601} 2 | -------------------------------------------------------------------------------- /data/keys/public.key: -------------------------------------------------------------------------------- 1 | {"Curvname":"P-256","X":74695617477160058757747208220371236837474210247114418775262229497812962582435,"Y":51348715319124770392993866417088542497927816017012182211244120852620959209571} -------------------------------------------------------------------------------- /data/wasm/counter.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuperchain/xuper-python-sdk/276562f193121530c4ac22008c702580809f2cb6/data/wasm/counter.wasm -------------------------------------------------------------------------------- /demo.py: -------------------------------------------------------------------------------- 1 | #encoding=utf8 2 | #!/bin/env python 3 | 4 | import xuper 5 | import time 6 | import random 7 | import json 8 | 9 | pysdk = xuper.XuperSDK("http://localhost:8098", "xuper") 10 | pysdk.readkeys("./data/keys") 11 | 12 | #0. 系统状态 13 | print(pysdk.system_status()) 14 | 15 | #1. 普通转账 16 | txid = pysdk.transfer("bob", 88888, desc="hello world") 17 | print(pysdk.balance("bob")) 18 | print(pysdk.query_tx(txid)) 19 | 20 | #2. 存证上链(不涉及UTXO,对应xuperunion > v3.4) 21 | #a. 读取文件 22 | with open('./data/evidence/demo.json', 'r') as f: 23 | data = json.load(f) 24 | #b. 文件反序列化 25 | txid = pysdk.transfer("bob", 0, desc=str(data)) 26 | print(pysdk.query_tx(txid)) 27 | 28 | #2. 创建合约账号 29 | new_account_name = pysdk.new_account() 30 | print("wait acl confirmed....") 31 | time.sleep(4) 32 | print("new account:", new_account_name) 33 | 34 | #3. 部署合约 35 | pysdk.transfer(new_account_name, 10000000, desc="start funds") 36 | 37 | pysdk.set_account(new_account_name) 38 | contract_name = 'counter'+str(random.randint(100,1000000)) 39 | print("contract name:", contract_name) 40 | print("deploying......") 41 | rsps = pysdk.deploy(new_account_name, contract_name, open('./data/wasm/counter.wasm','rb').read(), {'creator':b'baidu'}) 42 | print(rsps) 43 | 44 | #4. 预执行合约 45 | rsps = pysdk.preexec(contract_name, "get", {"key":b"counter"}) 46 | print(rsps.decode()) 47 | 48 | 49 | #5. 调用合约并生效上链 50 | for i in range(5): 51 | rsps = pysdk.invoke(contract_name, "increase", {"key":b"counter"}) 52 | print(rsps) 53 | print(pysdk.balance(new_account_name)) 54 | 55 | -------------------------------------------------------------------------------- /images/wallet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuperchain/xuper-python-sdk/276562f193121530c4ac22008c702580809f2cb6/images/wallet.png -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from distutils.core import setup 3 | LONGDOC = """ 4 | A pure python sdk for XuperChain 5 | """ 6 | 7 | setup(name='xuper', 8 | version='0.05', 9 | description='Pure Python SDK for XuperChain', 10 | long_description=LONGDOC, 11 | author='Sun, Junyi', 12 | author_email='ccnusjy@gmail.com', 13 | url='https://github.com/xuperchain/xuperunion', 14 | license="MIT", 15 | classifiers=[ 16 | 'Intended Audience :: Developers', 17 | 'License :: OSI Approved :: MIT License', 18 | 'Operating System :: OS Independent', 19 | 'Natural Language :: Chinese (Simplified)', 20 | 'Natural Language :: Chinese (Traditional)', 21 | 'Programming Language :: Python', 22 | 'Programming Language :: Python :: 2.7', 23 | 'Programming Language :: Python :: 3', 24 | 'Programming Language :: Python :: 3.2', 25 | 'Programming Language :: Python :: 3.3', 26 | 'Programming Language :: Python :: 3.4', 27 | 'Programming Language :: Python :: 3.5', 28 | 'Programming Language :: Python :: 3.6', 29 | ], 30 | install_requires=[ 31 | "requests", 32 | "ecdsa", 33 | ], 34 | keywords='Blockchain,XuperChain,Smart Contract', 35 | packages=['xuper'], 36 | package_dir={'xuper':'xuper'}, 37 | package_data={'xuper':['*.*']} 38 | ) 39 | -------------------------------------------------------------------------------- /wallet.py: -------------------------------------------------------------------------------- 1 | from tkinter import * 2 | from tkinter import ttk 3 | import xuper 4 | import json 5 | sdk = xuper.XuperSDK("http://localhost:8098", "xuper") 6 | sdk.readkeys('data/keys') 7 | 8 | def make_menus(): 9 | menubar = Menu(r) 10 | 11 | account = Menu(menubar) 12 | account.add_command(label="New Keys") 13 | account.add_command(label="Load Keys") 14 | account.add_command(label="New Account") 15 | menubar.add_cascade(label="Account", menu=account) 16 | 17 | contract = Menu(menubar) 18 | contract.add_command(label="Deploy") 19 | contract.add_command(label="Invoke") 20 | menubar.add_cascade(label="Contract", menu=contract) 21 | 22 | blocks = Menu(menubar) 23 | blocks.add_command(label="All") 24 | blocks.add_command(label="Query") 25 | menubar.add_cascade(label="Blocks", menu=blocks) 26 | 27 | transactions = Menu(menubar) 28 | transactions.add_command(label="All") 29 | transactions.add_command(label="Query") 30 | menubar.add_cascade(label="Transactions", menu=transactions) 31 | r.config(menu = menubar) 32 | 33 | def make_front(): 34 | def transfer_token(): 35 | global toAddr, toAmount,balance, tree 36 | txid = sdk.transfer(toAddr.get(), toAmount.get(), desc="demo") 37 | balance.set(sdk.balance(sdk.address)) 38 | txHistory.insert("", "end", text = txid, values = (toAddr.get(),toAmount.get())) 39 | 40 | def show_tx(event): 41 | item = txHistory.selection()[0] 42 | txid = txHistory.item(item,"text") 43 | theTx = sdk.query_tx(txid) 44 | t1 = Toplevel(r) 45 | txt = Text(t1, width=30, height=20) 46 | txt.grid(row=0, column=0) 47 | txt.insert(END, json.dumps(theTx)) 48 | t1.mainloop() 49 | 50 | Label(r, text = "My Address", font='Helvetica 14 bold').grid(row=0, column=0,sticky=W, padx=5, pady=2) 51 | Label(r, textvariable=myaddr).grid(row=0, column=1,sticky=E) 52 | Label(r, text = "Balance", font='Helvetica 14 bold').grid(row=1, column=0, sticky=W, padx=5, pady=2) 53 | Label(r, textvariable=balance).grid(row=1, column=1,sticky=E) 54 | txFrame = LabelFrame(r, bd=2, text="Transfer") 55 | txFrame.grid(row=2,column=0, columnspan=2, pady=10,padx=5) 56 | Label(txFrame, text="Receiver", font='Helvetica 12 bold').grid(row=0, column=1,padx=10, pady=10) 57 | Entry(txFrame, textvariable=toAddr, width=50).grid(row=0, column=2, padx=10, pady=10) 58 | Label(txFrame, text="Amount", font='Helvetica 12 bold').grid(row=1, column=1,padx=10, pady=10) 59 | Entry(txFrame, textvariable=toAmount, width=50).grid(row=1, column=2, padx=10, pady=10) 60 | Button(txFrame, text="Send", width="60", command=transfer_token).grid(row=2,column=0, padx=10, pady=10,columnspan=3) 61 | txHistory= ttk.Treeview(r, column=("Receiver", "Amount")) 62 | txHistory.heading("#0", text="Txid") 63 | txHistory.heading("#1", text="Receiver") 64 | txHistory.heading("#2", text="Amount") 65 | txHistory.grid(row=3,column=0,columnspan=2, pady=10,padx=5) 66 | txHistory.bind("", show_tx) 67 | 68 | r = Tk() 69 | myaddr = StringVar() 70 | myaddr.set(sdk.address) 71 | balance = IntVar() 72 | balance.set(sdk.balance(sdk.address)) 73 | toAddr = StringVar() 74 | toAddr.set("") 75 | toAmount = IntVar() 76 | toAmount.set(0) 77 | r.title("Wallet Demo") 78 | make_menus() 79 | make_front() 80 | r.mainloop() 81 | -------------------------------------------------------------------------------- /xuper/__init__.py: -------------------------------------------------------------------------------- 1 | from .client import XuperSDK 2 | __VERSION__ = "0.1" 3 | 4 | -------------------------------------------------------------------------------- /xuper/client.py: -------------------------------------------------------------------------------- 1 | #encoding=utf8 2 | #!/bin/env python 3 | import time 4 | import json 5 | import random 6 | import requests 7 | import ecdsa 8 | from ecdsa.util import sigencode_der, sigdecode_der 9 | import base64 10 | import hashlib 11 | import binascii 12 | import codecs 13 | from pprint import pprint 14 | from collections import OrderedDict, namedtuple 15 | from ecdsa import ellipticcurve, NIST256p, SigningKey, VerifyingKey 16 | 17 | TxTemplate = ''' 18 | { "txid":"", 19 | "tx_inputs": [ 20 | ], 21 | "tx_outputs": [ 22 | ], 23 | "desc": "", 24 | "nonce": "", 25 | "timestamp": "", 26 | "version": 1, 27 | "tx_inputs_ext":[ 28 | ], 29 | "tx_outputs_ext":[ 30 | ], 31 | "initiator": "", 32 | "auth_require": [ 33 | ], 34 | "initiator_signs": [ 35 | ], 36 | "auth_require_signs": [ 37 | ] 38 | } 39 | ''' 40 | 41 | ResTypeEnum = { 42 | "CPU":0, 43 | "MEMORY":1, 44 | "DISK":2, 45 | "XFEE":3 46 | } 47 | 48 | InvokeResponse = namedtuple('InvokeResponse', ['result', 'fee', 'txid']) 49 | 50 | def double_sha256(data): 51 | s1 = hashlib.sha256(data).digest() 52 | return hashlib.sha256(s1) 53 | 54 | def go_style_dumps(data): 55 | return json.dumps(data,separators=(',',':'),sort_keys=False) 56 | 57 | def to_bytes(n, length=32, endianess='big'): 58 | h = b'%x' % n 59 | s = codecs.decode((b'0'*(len(h) % 2) + h).zfill(length*2), 'hex') 60 | return s if endianess == 'big' else s[::-1] 61 | 62 | class XuperSDK(object): 63 | """ 64 | XuperSDK is a pure python sdk for xuperchain 65 | """ 66 | def __init__(self, url, bcname = 'xuper'): 67 | """ 68 | url is the address of xuperchain http gateway 69 | bcname is the chain name. e.g. xuper 70 | """ 71 | self.url = url 72 | self.bcname = bcname 73 | self.address = "" 74 | self.account_name = "" 75 | 76 | def __encodeTx(self, tx, include_sign = False): 77 | s = "" 78 | if tx['tx_inputs'] != None: 79 | for tx_input in tx['tx_inputs']: 80 | s += go_style_dumps(tx_input['ref_txid']) 81 | s += "\n" 82 | s += go_style_dumps(tx_input['ref_offset']) 83 | s += "\n" 84 | s += go_style_dumps(tx_input['from_addr']) 85 | s += "\n" 86 | if len(tx_input['amount']) > 0: 87 | s += go_style_dumps(tx_input['amount']) 88 | s += "\n" 89 | s += go_style_dumps(tx_input.get("frozen_height",0)) 90 | s += "\n" 91 | for out_item in tx['tx_outputs']: 92 | if 'amount' in out_item and len(out_item['amount']) == 0: 93 | del out_item['amount'] 94 | s += go_style_dumps(tx['tx_outputs']) 95 | s += "\n" 96 | if len(tx['desc']) > 0: 97 | s += go_style_dumps(tx['desc']) 98 | s += "\n" 99 | s += go_style_dumps(tx['nonce']) 100 | s += "\n" 101 | s += go_style_dumps(tx['timestamp']) 102 | s += "\n" 103 | s += go_style_dumps(tx['version']) 104 | s += "\n" 105 | for tx_input_ext in tx['tx_inputs_ext']: 106 | s += go_style_dumps(tx_input_ext['bucket']) 107 | s += "\n" 108 | s += go_style_dumps(tx_input_ext['key']) 109 | s += "\n" 110 | if 'ref_txid' in tx_input_ext: 111 | s += go_style_dumps(tx_input_ext['ref_txid']) 112 | s += "\n" 113 | s += go_style_dumps(tx_input_ext.get('ref_offset',0)) 114 | s += "\n" 115 | for tx_output_ext in tx['tx_outputs_ext']: 116 | s += go_style_dumps(tx_output_ext['bucket']) 117 | s += "\n" 118 | s += go_style_dumps(tx_output_ext['key']) 119 | s += "\n" 120 | s += go_style_dumps(tx_output_ext['value']) 121 | s += "\n" 122 | if 'contract_requests' not in tx: 123 | s += "null" # contract request 124 | s += "\n" 125 | else: 126 | s += go_style_dumps(tx['contract_requests']) 127 | s += "\n" 128 | s += go_style_dumps(tx['initiator']) 129 | s += "\n" 130 | s += go_style_dumps(tx['auth_require']) 131 | s += "\n" 132 | if include_sign: 133 | s += go_style_dumps(tx['initiator_signs']) 134 | s += "\n" 135 | s += go_style_dumps(tx['auth_require_signs']) 136 | s += "\n" 137 | s += "false\n" #coinbase 138 | s += "false\n" #autogen 139 | return s.encode() 140 | 141 | def __make_txid(self, tx): 142 | json_multi = self.__encodeTx(tx, True) 143 | #print(json_multi.decode()) 144 | return double_sha256(json_multi) 145 | 146 | def __my_address(self): 147 | if self.account_name != "": 148 | return self.account_name 149 | else: 150 | return self.address 151 | 152 | def __my_auth_require(self): 153 | if self.account_name != "": 154 | return self.account_name + "/" + self.address 155 | else: 156 | return self.address 157 | 158 | def __check_error(self, rsps_obj): 159 | if 'error' in rsps_obj: 160 | raise Exception(rsps_obj['error']) 161 | if 'error' in rsps_obj['header']: 162 | raise Exception(rsps_obj['header']) 163 | 164 | def __format_list(self, ary): 165 | for sub_item in ary: 166 | self.__format_obj(sub_item) 167 | return ary 168 | 169 | def __format_obj(self, obj): 170 | if type(obj) is not dict: 171 | return obj 172 | for k,v in obj.items(): 173 | if type(v) is list: 174 | self.__format_list(v) 175 | elif type(v) is dict: 176 | self.__format_obj(v) 177 | else: 178 | if k in ['txid','blockid', 'ref_txid','pre_hash', 'proposer','tip_blockid', 'root_blockid']: 179 | obj[k] = codecs.encode(codecs.decode(v.encode(), 'base64'), 'hex').decode() 180 | elif k in ['from_addr', 'to_addr']: 181 | obj[k] = codecs.decode(v.encode(), 'base64') 182 | elif k in ['amount']: 183 | obj[k] = int(codecs.encode(codecs.decode(v.encode(), 'base64'), 'hex'), 16) 184 | return obj 185 | 186 | def sign_tx(self, tx): 187 | """ 188 | must call read_keys to read private key first 189 | sign tx with private key, set signature in tx 190 | """ 191 | raw = self.__encodeTx(tx, False) 192 | #print(raw.decode()) 193 | s = self.private_key.sign(raw, hashfunc=double_sha256, sigencode=sigencode_der) 194 | tx['auth_require_signs'][0]['Sign'] = base64.b64encode(s).decode() 195 | tx['initiator_signs'][0]['Sign'] = base64.b64encode(s).decode() 196 | txid = self.__make_txid(tx).digest() 197 | tx['txid'] = base64.b64encode(txid).decode() 198 | 199 | def readkeys(self, path): 200 | """ 201 | read private keys from a directory, which must containser private.key, address and public.key 202 | """ 203 | self.address = open(path + "/address").read() 204 | self.private_key_js = open(path + "/private.key").read() 205 | self.public_key_js = open(path + "/public.key").read() 206 | sk_obj = json.loads(self.private_key_js) 207 | X = int(sk_obj['X']) 208 | Y = int(sk_obj['Y']) 209 | D = int(sk_obj['D']) 210 | self.public_key = VerifyingKey.from_public_point(ellipticcurve.Point(NIST256p.curve, X, Y), NIST256p, double_sha256) 211 | self.private_key = SigningKey.from_secret_exponent(D, NIST256p, double_sha256) 212 | #print(self.private_key.privkey.public_key.point.x()) 213 | #print(self.private_key.privkey.public_key.point.y()) 214 | 215 | def post_tx(self, tx): 216 | """ 217 | broadcast a tx to xchain node 218 | """ 219 | payload = { 220 | 'bcname':self.bcname, 221 | 'header':{'logid':'pysdk_post_tx'+str(int(time.time()*1e6)) }, 222 | 'txid': tx['txid'], 223 | 'tx': tx 224 | } 225 | #print(json.dumps(payload)) 226 | rsps = requests.post(self.url + "/v1/post_tx", data = json.dumps(payload)) 227 | rsps_obj = json.loads(rsps.content) 228 | self.__check_error(rsps_obj) 229 | return rsps.content 230 | 231 | def preexec(self, contract, method, args, module="wasm"): 232 | """ 233 | pre-execute a contract, and get response which contains inputs,outputs and response of contract" 234 | contract: contract name 235 | method: method name 236 | args: contract args 237 | module: contract module, default is wasm 238 | """ 239 | payload = { 240 | 'bcname':self.bcname, 241 | 'header':{'logid':'pysdk_preexec'+str(int(time.time())*1e6)}, 242 | 'requests':[ 243 | OrderedDict([ 244 | ('module_name', module), 245 | ('contract_name', contract), 246 | ('method_name', method), 247 | ('args',OrderedDict([(k,base64.b64encode(args[k]).decode()) for k in sorted(args.keys())])), 248 | ]) 249 | ], 250 | 'initiator':self.__my_address(), 251 | 'auth_require':[self.__my_auth_require()] 252 | } 253 | rsps = requests.post(self.url + "/v1/preexec", data = json.dumps(payload, sort_keys=False)) 254 | rsps_obj = json.loads(rsps.content) 255 | self.__check_error(rsps_obj) 256 | return rsps.content 257 | 258 | def query_tx(self, txid): 259 | """ 260 | query a transaction by txid (hex format) 261 | """ 262 | payload = { 263 | 'bcname':self.bcname, 264 | 'header':{'logid':'pysdk_query_tx'+str(int(time.time()*1e6)) }, 265 | 'txid': codecs.encode(codecs.decode(txid, 'hex'), 'base64').decode() 266 | } 267 | rsps = requests.post(self.url + "/v1/query_tx", data = json.dumps(payload)) 268 | rsps_obj = json.loads(rsps.content) 269 | self.__check_error(rsps_obj) 270 | return self.__format_obj(rsps_obj['tx']) 271 | 272 | def get_block(self, blockid): 273 | """ 274 | get a block by blockid (hex format) 275 | """ 276 | payload = { 277 | 'bcname':self.bcname, 278 | 'header':{'logid':'pysdk_get_block'+str(int(time.time()*1e6)) }, 279 | 'blockid': codecs.encode(codecs.decode(blockid, 'hex'), 'base64').decode(), 280 | 'need_content': True 281 | } 282 | rsps = requests.post(self.url + "/v1/get_block", data = json.dumps(payload)) 283 | rsps_obj = json.loads(rsps.content) 284 | self.__check_error(rsps_obj) 285 | return self.__format_obj(rsps_obj['block']) 286 | 287 | def system_status(self): 288 | """ 289 | get system status 290 | """ 291 | payload = { 292 | 'header':{'logid':'pysdk_system_status'+str(int(time.time()*1e6)) }, 293 | } 294 | rsps = requests.post(self.url + "/v1/get_sysstatus", data = json.dumps(payload)) 295 | rsps_obj = json.loads(rsps.content) 296 | self.__check_error(rsps_obj) 297 | return self.__format_obj(rsps_obj['systems_status']) 298 | 299 | def get_block_by_height(self, height): 300 | """ 301 | get a block by block height 302 | """ 303 | payload = { 304 | 'bcname':self.bcname, 305 | 'header':{'logid':'pysdk_get_block_by_height'+str(int(time.time()*1e6)) }, 306 | 'height': height 307 | } 308 | rsps = requests.post(self.url + "/v1/get_block_by_height", data = json.dumps(payload)) 309 | rsps_obj = json.loads(rsps.content) 310 | self.__check_error(rsps_obj) 311 | return self.__format_obj(rsps_obj['block']) 312 | 313 | 314 | def invoke(self, contract, method, args, module="wasm"): 315 | """ 316 | invoke a contract, then the state update will take effect on chain 317 | contract: contract name 318 | method: method name 319 | args: contract args 320 | module: module name, default: wasm 321 | """ 322 | rsps = self.preexec(contract, method, args, module) 323 | preexec_result = json.loads(rsps,object_pairs_hook=OrderedDict) 324 | return_msg = preexec_result['response']['response'] 325 | if 'gas_used' in preexec_result['response'].keys(): 326 | fee = preexec_result['response']['gas_used'] 327 | else: 328 | fee = 0 329 | if 'outputs' not in preexec_result['response']: 330 | return [base64.b64decode(x) for x in return_msg], int(fee) 331 | contract_info = {} 332 | contract_info['tx_inputs_ext'] = preexec_result['response']['inputs'] 333 | contract_info['tx_outputs_ext'] = preexec_result['response']['outputs'] 334 | contract_info['contract_requests'] = preexec_result['response']['requests'] 335 | contract_requests = contract_info["contract_requests"] 336 | for req in contract_requests: 337 | for res_limit in req['resource_limits']: 338 | if 'type' in res_limit: 339 | res_limit['type'] = ResTypeEnum[res_limit['type']] 340 | if 'limit' in res_limit: 341 | res_limit['limit'] = int(res_limit['limit']) 342 | txid = self.transfer('$', int(fee), '', contract_info) 343 | return InvokeResponse([base64.b64decode(x) for x in return_msg], int(fee), txid) 344 | 345 | def new_account(self, account_name=None, acl=None): 346 | """ 347 | create a new contract account 348 | account_name: name of the contract, should be 16 digits, if it is None, a random account will be generated 349 | """ 350 | if account_name == None: 351 | account_name = str(random.randint(0,9999999999999999)).zfill(16) 352 | if acl == None: 353 | acl = { 354 | "pm": { 355 | "rule": 1, 356 | "acceptValue": 1.0 357 | }, 358 | "aksWeight": { 359 | self.address: 1.0 360 | } 361 | } 362 | self.invoke('','NewAccount', {'account_name':account_name.encode(), 'acl':json.dumps(acl).encode()},'xkernel') 363 | return "XC"+account_name+"@"+self.bcname 364 | 365 | def set_account(self, account_name): 366 | """ 367 | set the account name represented by this SDK instance 368 | """ 369 | self.account_name = account_name 370 | 371 | def deploy(self, account_name, contract_name, code, init_args, runtime="c"): 372 | """ 373 | deploy a contract, only C or Go runtime supported 374 | account_name: account name 375 | contract_name: contract name 376 | code: wasm binary 377 | init_args: init call args 378 | runtime: runtime for wasm, e.g. "c" or "go" 379 | """ 380 | runtime_desc = { 381 | "c":b"\n\x01c", 382 | "go":b"\n\x02go" 383 | } 384 | js_init_args = go_style_dumps( 385 | OrderedDict([(k,base64.b64encode(init_args[k]).decode()) for k in sorted(init_args.keys())]) 386 | ) 387 | args = { 388 | 'account_name': account_name.encode(), 389 | 'contract_name': contract_name.encode(), 390 | 'contract_code': code, 391 | 'contract_desc': runtime_desc[runtime], 392 | 'init_args':js_init_args.encode() 393 | } 394 | return self.invoke('','Deploy', args, 'xkernel') 395 | 396 | def balance(self, address = None): 397 | """ 398 | get balance of an address or account 399 | """ 400 | if address == None: 401 | address = self.address 402 | payload = { 403 | 'bcs':[{'bcname':self.bcname}], 404 | 'address': address 405 | } 406 | balance_response = requests.post(self.url + "/v1/get_balance", data = json.dumps(payload)) 407 | balance = json.loads(balance_response.content) 408 | return balance['bcs'][0]['balance'] 409 | 410 | def transfer(self, to_address, amount, desc='', contract_info = None): 411 | """ 412 | transfer token to another address 413 | to_address: receiver 414 | amount: how much to be transfered 415 | desc: note 416 | contract_info: only needed when a contract is invoked. using invoke instead, if in that case 417 | """ 418 | payload = { 419 | 'bcname':self.bcname, 420 | 'address': self.__my_address(), 421 | 'totalNeed': str(amount), 422 | 'header':{'logid':'pysdk_'+str(int(time.time()*1e6)) }, 423 | 'needLock': False 424 | } 425 | select_response = requests.post(self.url + "/v1/select_utxos_v2", data = json.dumps(payload)) 426 | selected_obj = json.loads(select_response.content) 427 | self.__check_error(selected_obj) 428 | tx = json.loads(TxTemplate) 429 | #pprint(selected_obj) 430 | if 'utxoList' in selected_obj: 431 | tx['tx_inputs'] = selected_obj['utxoList'] 432 | else: 433 | tx['tx_inputs'] = None 434 | 435 | if tx['tx_inputs'] != None: 436 | for x in tx['tx_inputs']: 437 | x['ref_txid'] = x['refTxid'] 438 | x['ref_offset'] = x.get('refOffset', 0) 439 | x['from_addr'] = base64.b64encode(self.__my_address().encode()).decode() 440 | del x['refTxid'] 441 | del x['toAddr'] 442 | if 'refOffset' in x: 443 | del x['refOffset'] 444 | total_selected = int(selected_obj['totalSelected']) 445 | output_return = total_selected - amount 446 | tx['tx_outputs'].append( 447 | { 448 | 'amount':base64.b64encode(to_bytes(amount).lstrip(b'\0')).decode(), 449 | 'to_addr': base64.b64encode(to_address.encode('ascii')).decode() 450 | } 451 | ) 452 | if output_return > 0: 453 | tx['tx_outputs'].append( 454 | { 455 | 'amount':base64.b64encode(to_bytes(output_return).lstrip(b'\0')).decode(), 456 | 'to_addr': base64.b64encode(self.__my_address().encode()).decode() 457 | } 458 | ) 459 | tx['desc'] = base64.b64encode(desc.encode()).decode() 460 | tx['nonce'] = str(int(time.time()*1e6)) 461 | tx['timestamp'] = int(time.time()*1e6) 462 | tx['initiator'] = self.__my_address() 463 | tx['auth_require'].append(self.__my_auth_require()) 464 | tx['initiator_signs'].append({ 465 | 'PublicKey':self.public_key_js, 466 | 'Sign':'' 467 | }) 468 | tx['auth_require_signs'].append({ 469 | 'PublicKey':self.public_key_js, 470 | 'Sign':'' 471 | }) 472 | if contract_info != None: 473 | tx.update(contract_info) 474 | self.sign_tx(tx) 475 | #print(json.dumps(tx)) 476 | res = self.post_tx(tx) 477 | return codecs.encode(codecs.decode(tx['txid'].encode(),'base64'), 'hex').decode() 478 | 479 | --------------------------------------------------------------------------------