Skip to content

Commit

Permalink
Add common contracts
Browse files Browse the repository at this point in the history
  • Loading branch information
Rexagon committed Apr 2, 2024
1 parent 8bd262f commit fcd0dd0
Show file tree
Hide file tree
Showing 6 changed files with 376 additions and 134 deletions.
1 change: 1 addition & 0 deletions python/nekoton/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from .nekoton import *

from . import gql
from . import contracts
3 changes: 3 additions & 0 deletions python/nekoton/contracts/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .base import IGiver
from .giver import GiverV1
from .ever_wallet import EverWallet
10 changes: 10 additions & 0 deletions python/nekoton/contracts/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import nekoton as _nt


class IGiver:
"""
Abstract tokens giver.
"""

async def give(self, target: _nt.Address, amount: _nt.Tokens):
raise NotImplementedError("IGiver is an abstract class")
123 changes: 123 additions & 0 deletions python/nekoton/contracts/ever_wallet.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
from typing import Optional

from ... import base as _base
import nekoton as _nt

_wallet_abi = _nt.ContractAbi("""{
"ABI version": 2,
"version": "2.3",
"header": ["pubkey", "time", "expire"],
"functions": [{
"name": "sendTransaction",
"inputs": [
{"name": "dest", "type": "address"},
{"name": "value", "type": "uint128"},
{"name": "bounce", "type": "bool"},
{"name": "flags", "type": "uint8"},
{"name": "payload", "type": "cell"}
],
"outputs": []
}],
"events": []
}""")

_send_transaction = _wallet_abi.get_function("sendTransaction")
assert _send_transaction is not None

_wallet_code = _nt.Cell.decode(
"te6cckEBBgEA/AABFP8A9KQT9LzyyAsBAgEgAgMABNIwAubycdcBAcAA8nqDCNcY7UTQgwfXAdcLP8j4KM8WI88WyfkAA3HXAQHDAJqDB9cBURO68uBk3oBA1wGAINcBgCDXAVQWdfkQ8qj4I7vyeWa++COBBwiggQPoqFIgvLHydAIgghBM7mRsuuMPAcjL/8s/ye1UBAUAmDAC10zQ+kCDBtcBcdcBeNcB10z4AHCAEASqAhSxyMsFUAXPFlAD+gLLaSLQIc8xIddJoIQJuZgzcAHLAFjPFpcwcQHLABLM4skB+wAAPoIQFp4+EbqOEfgAApMg10qXeNcB1AL7AOjRkzLyPOI+zYS/"
)
_wallet_data_abi = [
("publicKey", _nt.AbiUint(256)),
("timestamp", _nt.AbiUint(64)),
]


class EverWallet(_base.IGiver):
@classmethod
def compute_address(
cls, public_key: _nt.PublicKey, workchain: int = 0
) -> _nt.Address:
return cls.compute_state_init(public_key).compute_address(workchain)

@staticmethod
def compute_state_init(public_key: _nt.PublicKey) -> _nt.StateInit:
data = _nt.Cell.build(
abi=_wallet_data_abi,
value={
"publicKey": public_key,
"timestamp": 0,
},
)
return _nt.StateInit(_wallet_code, data)

def __init__(
self, transport: _nt.Transport, keypair: _nt.KeyPair, workchain: int = 0
):
state_init = self.compute_state_init(keypair.public_key)

self._initialized = False
self._transport = transport
self._keypair = keypair
self._state_init = state_init
self._address = state_init.compute_address(workchain)

@property
def address(self) -> _nt.Address:
return self._address

async def give(self, target: _nt.Address, amount: _nt.Tokens):
await self.send(dst=target, value=amount, payload=_nt.Cell(), bounce=False)

async def send(
self,
dst: _nt.Address,
value: _nt.Tokens,
payload: _nt.Cell,
bounce: bool = False,
) -> _nt.Transaction:
state_init = await self.__get_state_init()

signature_id = await self._transport.get_signature_id()

external_message = _send_transaction.encode_external_message(
self._address,
input={
"dest": dst,
"value": value,
"bounce": bounce,
"flags": 3,
"payload": payload,
},
public_key=self._keypair.public_key,
state_init=state_init,
).sign(self._keypair, signature_id)

tx = await self._transport.send_external_message(external_message)
if tx is None:
raise RuntimeError("Message expired")
return tx

async def get_account_state(self) -> Optional[_nt.AccountState]:
return await self._transport.get_account_state(self._address)

async def get_balance(self) -> _nt.Tokens:
state = await self.get_account_state()
if state is None:
return _nt.Tokens(0)
else:
return state.balance

async def __get_state_init(self) -> Optional[_nt.StateInit]:
if self._initialized:
return None

account_state = await self.get_account_state()
if (
account_state is not None
and account_state.status == _nt.AccountStatus.Active
):
self._initialized = True
return None
else:
return self._state_init
105 changes: 105 additions & 0 deletions python/nekoton/contracts/giver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
from __future__ import annotations

from ... import base as _base
import nekoton as _nt

_giver_v1_abi = _nt.ContractAbi("""{
"ABI version": 1,
"functions": [{
"name": "constructor",
"inputs": [],
"outputs": []
}, {
"name": "sendGrams",
"inputs": [
{"name": "dest", "type": "address"},
{"name": "amount", "type": "uint64"}
],
"outputs": []
}],
"events": []
}""")

_giver_v1_constructor = _giver_v1_abi.get_function("constructor")
_giver_v1_send_grams = _giver_v1_abi.get_function("sendGrams")
_giver_v1_tvc = "te6ccgECJQEABaMAAgE0BgEBAcACAgPPIAUDAQHeBAAD0CAAQdgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAIo/wAgwAH0pCBYkvSg4YrtU1gw9KATBwEK9KQg9KEIAgPNQBAJAgHODQoCASAMCwAHDDbMIAAdPAZIbzyvCEhcHHwCl8CgAgEgDw4AASAA1T++wFkZWNvZGVfYWRkciD6QDL6QiBvECByuiFzurHy4H0hbxFu8uB9yHTPCwIibxLPCgcibxMicrqWI28TIs4ynyGBAQAi10mhz0AyICLOMuL+/AFkZWNvZGVfYWRkcjAhydAlVUFfBdswgAgEgEhEAK6T/fYCzsrovsTC2MLcxsvwTt4htmEAApaV/fYCwsa+6OTC3ObMyuWQ5Z6ARZ4UAOOegfBRnixJnixH9ATjnoDh9ATh9AUAgZ6B8EeeFj7lnoBBkkX2Af3+AsLGvujkwtzmzMrkvsrcyL4LAAgEgGhQB4P/+/QFtYWluX2V4dGVybmFsIY5Z/vwBZ2V0X3NyY19hZGRyINAg0wAycL2OGv79AWdldF9zcmNfYWRkcjBwyMnQVRFfAtsw4CBy1yExINMAMiH6QDP+/QFnZXRfc3JjX2FkZHIxISFVMV8E2zDYMSEVAfiOdf7+AWdldF9tc2dfcHVia2V5IMcCjhb+/wFnZXRfbXNnX3B1YmtleTFwMdsw4NUgxwGOF/7/AWdldF9tc2dfcHVia2V5MnAxMdsw4CCBAgDXIdcL/yL5ASIi+RDyqP7/AWdldF9tc2dfcHVia2V5MyADXwPbMNgixwKzFgHMlCLUMTPeJCIijjj++QFzdG9yZV9zaWdvACFvjCJvjCNvjO1HIW+M7UTQ9AVvjCDtV/79AXN0b3JlX3NpZ19lbmRfBdgixwGOE/78AW1zZ19pc19lbXB0eV8G2zDgItMfNCPTPzUgFwF2joDYji/+/gFtYWluX2V4dGVybmFsMiQiVXFfCPFAAf7+AW1haW5fZXh0ZXJuYWwzXwjbMOCAfPLwXwgYAf7++wFyZXBsYXlfcHJvdHBwcO1E0CD0BDI0IIEAgNdFmiDTPzIzINM/MjKWgggbd0Ay4iIluSX4I4ED6KgkoLmwjinIJAH0ACXPCz8izws/Ic8WIMntVP78AXJlcGxheV9wcm90Mn8GXwbbMOD+/AFyZXBsYXlfcHJvdDNwBV8FGQAE2zACASAcGwAPvOP3EDmG2YQCASAeHQCJuyXMvJ+ADwINM/MPAi/vwBcHVzaHBkYzd0b2M07UTQ9AHI7UdvEgH0ACHPFiDJ7VT+/QFwdXNocGRjN3RvYzQwXwLbMIAgEgIh8BCbiJACdQIAH+/v0BY29uc3RyX3Byb3RfMHBwgggbd0DtRNAg9AQyNCCBAIDXRY4UINI/MjMg0j8yMiBx10WUgHvy8N7eyCQB9AAjzws/Is8LP3HPQSHPFiDJ7VT+/QFjb25zdHJfcHJvdF8xXwX4ADDwIf78AXB1c2hwZGM3dG9jNO1E0PQByCEARO1HbxIB9AAhzxYgye1U/v0BcHVzaHBkYzd0b2M0MF8C2zAB4tz+/QFtYWluX2ludGVybmFsIY5Z/vwBZ2V0X3NyY19hZGRyINAg0wAycL2OGv79AWdldF9zcmNfYWRkcjBwyMnQVRFfAtsw4CBy1yExINMAMiH6QDP+/QFnZXRfc3JjX2FkZHIxISFVMV8E2zDYJCFwIwHqjjj++QFzdG9yZV9zaWdvACFvjCJvjCNvjO1HIW+M7UTQ9AVvjCDtV/79AXN0b3JlX3NpZ19lbmRfBdgixwCOHCFwuo4SIoIQXH7iB1VRXwbxQAFfBtsw4F8G2zDg/v4BbWFpbl9pbnRlcm5hbDEi0x80InG6JAA2niCAI1VhXwfxQAFfB9sw4CMhVWFfB/FAAV8H"


class GiverV1(_base.IGiver):
@staticmethod
def compute_address(workchain: int = 0) -> _nt.Address:
return _nt.Address.from_parts(
workchain, _nt.Cell.decode(_giver_v1_tvc).repr_hash
)

@staticmethod
async def deploy(
transport: _nt.Transport,
workchain: int = 0,
other_giver: _base.IGiver | None = None,
) -> GiverV1:
# Compute giver address
state_init_cell = _nt.Cell.decode(_giver_v1_tvc)
address = _nt.Address.from_parts(workchain, state_init_cell.repr_hash)
state_init = _nt.StateInit.from_cell(state_init_cell)

# Ensure that giver account exists
initial_balance = _nt.Tokens(1)
state = await transport.get_account_state(address)
if state is None:
if other_giver is None:
raise RuntimeError("Account does not have enough balance")

tx = await other_giver.give(address, initial_balance)
if tx is None:
raise RuntimeError("Message expired")
await transport.trace_transaction(tx).wait()

# Deploy account
if state.status == _nt.AccountStatus.Active:
return GiverV1(transport, workchain)
elif state.status == _nt.AccountStatus.Frozen:
raise RuntimeError("Giver account is frozen")
elif (
state.status == _nt.AccountStatus.Uninit and state.balance < initial_balance
):
tx = await other_giver.give(address, initial_balance)
if tx is None:
raise RuntimeError("Message expired")
await transport.trace_transaction(tx).wait()

external_message = _giver_v1_constructor.encode_external_message(
address,
input={},
public_key=None,
state_init=state_init,
).without_signature()
tx = await transport.send_external_message(external_message)
if tx is None:
raise RuntimeError("Message expired")
await transport.trace_transaction(tx).wait()

return GiverV1(transport, workchain)

def __init__(self, transport: _nt.Transport, workchain: int = 0):
self._transport = transport
self._address = GiverV1.compute_address(workchain)

async def give(self, target: _nt.Address, amount: _nt.Tokens):
# Prepare external message
message = _giver_v1_send_grams.encode_external_message(
self._address,
input={
"dest": target,
"amount": amount,
},
public_key=None,
).without_signature()

# Send external message
tx = await self._transport.send_external_message(message)
if tx is None:
raise RuntimeError("Message expired")

# Wait until all transactions are produced
await self._transport.trace_transaction(tx).wait()
Loading

0 comments on commit fcd0dd0

Please sign in to comment.