-
Notifications
You must be signed in to change notification settings - Fork 8
Sample Code for Payment Channel Contract
To generate and broadcast transactions, we can use pyvsystems to use contract functionalities on vsys chain.
pyvsystems can be installed by first cloning the github repository
git clone https://github.com/virtualeconomy/pyvsystems.git
Then using pip, we can install the repository as a package
pip3 install pyvsystems/.
Then we can simply import pyvsystems in your own workplace
Import package pyvystems
import pyvsystems as pv
from pyvsystems import Account
from pyvsystems.crypto import verifySignature
from pyvsystems.contract_helper import *To broadcast any VSYS transactions on chain, we must first initialize API parameters to a full node, and we will also need the seed to an account with VSYS coins.
Initialize API connection
# Initialize API connection. API key is needed if you intend to interact with the node's database.
custom_wrapper = pv.create_api_wrapper("<your_node_address>", api_key="<your_node_api_key (optional)>")
# chain = pv.Chain(chain_name='mainnet', chain_id='M', address_version=5, api_wrapper=custom_wrapper)
chain = pv.Chain(chain_name='testnet', chain_id='T', address_version=5, api_wrapper=custom_wrapper)
sender = Account(chain=chain, seed="<your_sender_seed>")VSYS contracts are defined by an encoded byte string that contains the information of the contract functions. We can get this information by the use of the contract helper classes.
# Contract object to be registered\
payment_channel_helper = PaymentChannelContractHelper()
payment_channel_contract_object = payment_channel_helper.contract_objectThe code samples below show how to use the VSYSPY python wrapper to use a payment channel that can store VSYS coins or V Coins (VSYS' native coin). But any token can be used as long as the contract ID and token ID of the token is known.
Note that smart contracts in VSYS take in something called DataEntry, which specifies the value and type of the input. The functions in contracts have specific types of inputs, and the types must match in order to utilise the contract.
When we register a payment channel contract, we must specify the token id of what this contract can store and use. It will be unable to store or use any other token.
Register payment channel contract that can store tokens
Payment Channel Contract ByteString:
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
| Triggers | Inputs | Input Types | Description |
|---|---|---|---|
| Init | ("tokenID") | (token_id) | Triggered when you register a Payment Channel Contract that stores a specific Token |
# Token id. Simply replace token id with your own
token_id = "<your_token_id>"
register_payment_channel_data_stack = payment_channel_helper.register_data_stack_generator(token_id)
payment_channel_contract_object = payment_channel_helper.contract_object
sender.register_contract(payment_channel_contract_object, register_payment_channel_data_stack)Register payment channel contract that can store VSYS coins
Note that the vsys system contract / token id may be different on the mainnet compared to testnet.
token_id = "TWuKDNU1SAheHR99s1MbGZLPh1KophEmKk1eeU3mW"
register_payment_channel_data_stack = payment_channel_helper.register_data_stack_generator(token_id)
payment_channel_contract_object = payment_channel_helper.contract_object
sender.register_contract(payment_channel_contract_object, register_payment_channel_data_stack)Deposit some VSYS / tokens into the channel
Before a payment channel can be opened, the contract itself must contain some funds to fund the underlying channel. Make sure you record the contract id of your payment channel contract.
Depositing tokens
Note that tokens are controlled by the token's contract.
token_contract_without_split_helper = TokenWithoutSplitContractHelper()
token_contract_id = "<your_token_contract_id>"
payment_channel_contract_id = "<your_payment_channel_contract_id>"
deposit_function_id = token_contract_without_split_helper.deposit_function_index
deposit_data_stack = token_contract_without_split_helper.deposit_data_stack_generator(sender.address, payment_channel_contract_id, <your_deposit_amount>)
sender.execute_contract(token_contract_id, deposit_function_id, deposit_data_stack)Depositing VSYS Coins
Note that VSYS coins are controlled by the system contract. The system contract id may be different for testnet and mainnet.
system_contract_helper = SystemContractHelper()
testnet_system_contract_id = "CF9Nd9wvQ8qVsGk8jYHbj6sf8TK7MJ2GYgt"
payment_channel_contract_id = "<your_payment_channel_contract_id>"
deposit_function_id = token_contract_without_split_helper.deposit_function_index
deposit_data_stack = system_contract_helper.deposit_data_stack_generator(sender.address, payment_channel_contract_id, <your_deposit_amount>)
sender.execute_contract(testnet_system_contract_id, deposit_function_id, deposit_data_stack)
Once the payment channel contract has been registered and funds has been deposited into it, it's important to remember that the payment channel still needs to be opened, specifying who the recipient will be.
Then, a one-way payment channel will be opened between the sender and the recipient, where the sender will be able to give signatures of payment transactions to the recipient off-chain. To use the functions of the contract, we must broadcast the correct data entries with the corresponding function id.
| Function | function id | Inputs | Input DataEntry Types | Description |
|---|---|---|---|---|
| Create and load | 0 | ("recipient", "amount", "expirationTime") | (address, amount, timestamp) | Creates the payment channel and loads an amount into it (this function's transaction id becomes the channel id) |
| Extend Expiration Time | 1 | ("channelId", "expirationTime") | (short_bytes, timestamp) | Extends the expiration time of the channel to the new input timestamp |
| Load | 2 | ("channelId", "amount") | (short_bytes, amount) | Loads more tokens into the channel |
| Abort | 3 | ("channelId") | (short_bytes) | Abort the channel, triggering a 2 day grace period (where the recipient can still collect payments). After 2 days, the payer can unload all the remaining funds that was locked in the channel |
| Unload | 4 | ("channelId") | (short_bytes) | Unload all the funds locked in the channel (only works if the channel has expired) |
| Collect Payment | 5 | ("channelId", "amount", "signature") | (short_bytes, amount, short_bytes) | Allows the recipient to collect payments he has a valid signature for |
# Create and load payment channel using payment channel contract
# In a real usage scenario, we would not know the recipient's seed, then we can simply set the recipient's address
# recipient = Account(chain=chain, address='<recipient-address>')
recipient = Account(chain=chain, seed="<your_recipient_seed>")
payment_channel_helper = PaymentChannelContractHelper()
payment_channel_contract_id = "<your_payment_channel_contract_id>"
create_and_load_function_id = payment_channel_helper.create_and_load_function_index
create_and_load_data_stack = payment_channel_helper.create_and_load_data_stack_generator(recipient.address, <your_load_amount>, <your_expiration_timestamp>)
sender.execute_contract(payment_channel_contract_id, create_and_load_function_id, create_and_load_data_stack)The channel ID is created from the transaction ID of the create and load function, which acts as a unique key to each payment channel. Make sure to record this properly.
# Channel id and channel contract id is used for every channel function
channel_id = "<your_channel_id (transaction id of your create and load function)>"Updating the expiration timestamp allows users to continue using the same payment channel for as long as they wish. If the channel ever expires or runs out of funds, simply extend the expiration timestamp, load more funds into the channel, and continue using it.
# Update exipration timestamp of payment channel
payment_channel_helper = PaymentChannelContractHelper()
payment_channel_contract_id = "<your_payment_channel_contract_id>"
channel_id = "<your_channel_id (transaction id of your create and load function)>"
extend_expiration_time_function_id = payment_channel_helper.extend_expiration_time_function_index
extend_expiration_time_data_stack = payment_channel_helper.extend_expiration_time_data_stack_generator(channel_id, <your_new_expiration_timestamp>)
sender.execute_contract(payment_channel_contract_id, extend_expiration_time_function_id, extend_expiration_time_data_stack)# Load more into payment channel
payment_channel_helper = PaymentChannelContractHelper()
payment_channel_contract_id = "<your_payment_channel_contract_id>"
channel_id = "<your_channel_id (transaction id of your create and load function)>"
load_function_id = payment_channel_helper.load_function_index
load_data_stack = payment_channel_helper.load_data_stack_generator(channel_id, <your_load_amount>)
sender.execute_contract(payment_channel_contract_id, load_function_id, load_data_stack)The sender can abort the channel and trigger the start of a timer, which allows 2 days for the recipient to still collect any uncollected payments. When either the channel was aborted more than 2 days ago, or the expiration timestamp has been passed, the sender can unload the uncollected funds back to their possession.
# Abort the channel
payment_channel_helper = PaymentChannelContractHelper()
payment_channel_contract_id = "<your_payment_channel_contract_id>"
channel_id = "<your_channel_id (transaction id of your create and load function)>"
abort_function_id = payment_channel_helper.abort_function_index
abort_data_stack = payment_channel_helper.abort_data_stack_generator(channel_id)
sender.execute_contract(payment_channel_contract_id, abort_function_id, abort_data_stack)# Unload the channel after it has expired
payment_channel_helper = PaymentChannelContractHelper()
payment_channel_contract_id = "<your_payment_channel_contract_id>"
channel_id = "<your_channel_id (transaction id of your create and load function)>"
unload_function_id = payment_channel_helper.unload_function_index
unload_data_stack = payment_channel_helper.unload_data_stack_generator(channel_id)
sender.execute_contract(payment_channel_contract_id, unload_function_id, unload_data_stack)
Payment Channels allow the payer to give payment signatures to the recipient off-chain. These can only be used to collect payments by the recipient, and so can be safely sent even over insecure channels. The contract helper contains a function that can produce these signatures using the payer's private key.
payment_channel_helper = PaymentChannelContractHelper()
channel_id = "<your_channel_id (transaction id of your create and load function)>"
payment_signature_str = payment_channel_helper.generate_off_chain_payment_signature(sender.privateKey, channel_id, <your_payment_amount>)The recipient can verify the validity of the signature by using the sender's public key (if this public key isn't known, it can be obtained by querying the contract's database, which is described in detail below). Note that the message input is in bytes. The output of this function is 0 for verified, and -1 for incorrect signatures.
channel_id = "<your_channel_id (transaction id of your create and load function)>"
channel_id_bytes = base58.b58decode(channel_id)
channel_id_bytes_with_length = struct.pack(">H", len(channel_id_bytes)) + channel_id_bytes
payment_amount_bytes = struct.pack(">Q", <your_payment_amount>)
message = channel_id_bytes_with_length + payment_amount_bytes
print(verifySignature(sender.publicKey, message, payment_signature_str))Then the recipient can broadcast this signature to the blockchain to obtain the promised funds.
# Broadcast collect payment
payment_channel_contract_id = "<your_payment_channel_contract_id>"
channel_id = "<your_channel_id (transaction id of your create and load function)>"
payment_signature_str = payment_channel_helper.generate_off_chain_payment_signature(sender.privateKey, channel_id, <your_payment_amount>)
collect_payment_function_id = payment_channel_helper.collect_payment_function_index
collect_payment_data_stack = payment_channel_helper.collect_payment_data_stack_generator(channel_id, <your_payment_amount>, payment_signature_str)
recipient.execute_contract(payment_channel_contract_id, collect_payment_function_id, collect_payment_data_stack)The funds now belong to the recipient, but will still remain in the contract, until the recipient decides to withdraw them using either the token contract (for tokens), or the system contract (for VSYS coins), then the funds will be withdrawn to their wallet.
In order for the contract to do anything, it has to store some information within the database. This information can be queried by using the correct database key within the full node. The contract helper objects contain the corresponding database keys for each stored variable.
| State Variable | State Variable Index | Description |
|---|---|---|
| maker | 0 | The address of the creator of the payment channel contract |
| tokenId | 1 | The token id of the token that can be stored by this payment channel contract |
| State Map | State Map Index | State Map Key | Description |
|---|---|---|---|
| contractBalance | 0 | userAddress (address) | The balance of tokens stored within this contract belonging to userAddress |
| channelCreator | 1 | channelId (short_bytes) | The address of the creator of the channel |
| channelCreatorPublicKey | 2 | channelId (short_bytes) | The public key of the creator of the channel |
| channelRecipient | 3 | channelId (short_bytes) | The address of the recipient of the channel |
| channelAccumulatedLoad | 4 | channelId (short_bytes) | The accumulated amount loaded into the channel |
| channelAccumulatedPayment | 5 | channelId (short_bytes) | The accumulated amount already collected by the recipient |
| channelExpirationTime | 6 | channelId (short_bytes) | The expiration time of the channel, after which the creator can unload the funds |
| channelStatus | 7 | channelId (short_bytes) | The status of the channel |
While it should be quite intuitive that userAddress is simply the address of interest, the channelId may not be immediately obvious.
The payment channel contract makes a distinction between the contract itself and its underlying channel, in fact, a single payment channel contract can create multiple channels.
A channel is therefore not distinguished by its contract, but by its channel id, the channel id is simply the transaction id of the transaction that created the channel (the transaction id of the "createAndLoad" function).
# Check the maker of the contract
payment_channel_helper = PaymentChannelContractHelper()
payment_channel_contract_id = "<your_payment_channel_contract_id>"
maker_db_key = payment_channel_helper.maker_db_key_generator()
print(chain.contract_db_query(payment_channel_contract_id, maker_db_key))# Check the related token id to the payment channel contract
payment_channel_helper = PaymentChannelContractHelper()
payment_channel_contract_id = "<your_payment_channel_contract_id>"
token_id_db_key = payment_channel_helper.token_id_db_key_generator()
print(chain.contract_db_query(payment_channel_contract_id, token_id_db_key))# Check address contract balance
payment_channel_helper = PaymentChannelContractHelper()
payment_channel_contract_id = "<your_payment_channel_contract_id>"
contract_balance_key = payment_channel_helper.contract_balance_db_key_generator(sender.address)
print(chain.contract_db_query(payment_channel_contract_id, contract_balance_key))# Check creator of channel's address
payment_channel_helper = PaymentChannelContractHelper()
payment_channel_contract_id = "<your_payment_channel_contract_id>"
channel_id = "<your_channel_id (transaction id of your create and load function)>"
creator_key = payment_channel_helper.channel_creator_db_key_generator(channel_id)
print(chain.contract_db_query(payment_channel_contract_id, creator_key))# Check creator of channel's public key
payment_channel_helper = PaymentChannelContractHelper()
payment_channel_contract_id = "<your_payment_channel_contract_id>"
channel_id = "<your_channel_id (transaction id of your create and load function)>"
creator_public_key_db_key = payment_channel_helper.channel_creator_public_key_db_key_generator(channel_id)
print(chain.contract_db_query(payment_channel_contract_id, creator_public_key_db_key))# Check recipient of channel's address
payment_channel_helper = PaymentChannelContractHelper()
payment_channel_contract_id = "<your_payment_channel_contract_id>"
channel_id = "<your_channel_id (transaction id of your create and load function)>"
recipient_key = payment_channel_helper.channel_recipient_db_key_generator(channel_id)
print(chain.contract_db_query(payment_channel_contract_id, recipient_key))# Check channel accumulated load
payment_channel_helper = PaymentChannelContractHelper()
payment_channel_contract_id = "<your_payment_channel_contract_id>"
channel_id = "<your_channel_id (transaction id of your create and load function)>"
channel_accumulated_load_key = payment_channel_helper.channel_accumulated_load_db_key_generator(channel_id)
print(chain.contract_db_query(payment_channel_contract_id, channel_accumulated_load_key))# Check accumulated payment Balance
payment_channel_helper = PaymentChannelContractHelper()
payment_channel_contract_id = "<your_payment_channel_contract_id>"
channel_id = "<your_channel_id (transaction id of your create and load function)>"
channel_accumulated_payment_key = payment_channel_helper.channel_accumulated_payment_db_key_generator(channel_id)
print(chain.contract_db_query(payment_channel_contract_id, channel_accumulated_payment_key))# Check expiration time
payment_channel_helper = PaymentChannelContractHelper()
payment_channel_contract_id = "<your_payment_channel_contract_id>"
channel_id = "<your_channel_id (transaction id of your create and load function)>"
channel_expiration_time_payment_key = payment_channel_helper.channel_expiration_time_db_key_generator(channel_id)
print(chain.contract_db_query(payment_channel_contract_id, channel_expiration_time_payment_key))# Check channel status
payment_channel_helper = PaymentChannelContractHelper()
payment_channel_contract_id = "<your_payment_channel_contract_id>"
channel_id = "<your_channel_id (transaction id of your create and load function)>"
channel_status_key = payment_channel_helper.channel_status_db_key_generator(channel_id)
print(chain.contract_db_query(payment_channel_contract_id, channel_status_key))