Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
218 changes: 217 additions & 1 deletion plugin/python/contract/contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@
PluginEndRequest,
PluginEndResponse,
MessageSend,
MessageReward,
MessageFaucet,
Faucet,
Reward,
PluginKeyRead,
PluginStateReadRequest,
PluginStateWriteRequest,
Expand Down Expand Up @@ -54,11 +58,14 @@
"name": "python_plugin_contract",
"id": 1,
"version": 1,
"supported_transactions": ["send"],
"supported_transactions": ["send", "reward", "faucet"],
"transaction_type_urls": [
"type.googleapis.com/types.MessageSend",
"type.googleapis.com/types.MessageReward",
"type.googleapis.com/types.MessageFaucet",
],
"event_type_urls": [],
"custom_state_prefixes": [FAUCET_PREFIX, REWARD_PREFIX],
# Include google/protobuf/any.proto first as it's a dependency of event.proto and tx.proto
"file_descriptor_protos": [
any_pb2.DESCRIPTOR.serialized_pb,
Expand All @@ -74,6 +81,8 @@
ACCOUNT_PREFIX = b"\x01"
POOL_PREFIX = b"\x02"
PARAMS_PREFIX = b"\x07"
FAUCET_PREFIX = b"\x64" # 100 - outside core reserved range 1-15
REWARD_PREFIX = b"\x65" # 101 - outside core reserved range 1-15


# Key generation functions (from keys.py)
Expand Down Expand Up @@ -115,6 +124,16 @@ def key_for_fee_pool(chain_id: int) -> bytes:
return join_len_prefix(POOL_PREFIX, format_uint64(chain_id))


def key_for_faucet(address: bytes) -> bytes:
"""Generate state database key for a faucet record."""
return join_len_prefix(FAUCET_PREFIX, address)


def key_for_reward(address: bytes) -> bytes:
"""Generate state database key for a reward record."""
return join_len_prefix(REWARD_PREFIX, address)


# Proto marshal/unmarshal utilities

def marshal(message: Any) -> bytes:
Expand Down Expand Up @@ -203,6 +222,14 @@ async def check_tx(self, request: PluginCheckRequest) -> PluginCheckResponse:
msg = MessageSend()
msg.ParseFromString(request.tx.msg.value)
return self._check_message_send(msg)
elif type_url.endswith("/types.MessageReward"):
msg = MessageReward()
msg.ParseFromString(request.tx.msg.value)
return self._check_message_reward(msg)
elif type_url.endswith("/types.MessageFaucet"):
msg = MessageFaucet()
msg.ParseFromString(request.tx.msg.value)
return self._check_message_faucet(msg)
else:
raise err_invalid_message_cast()

Expand All @@ -228,6 +255,14 @@ async def deliver_tx(self, request: PluginDeliverRequest) -> PluginDeliverRespon
msg = MessageSend()
msg.ParseFromString(request.tx.msg.value)
return await self._deliver_message_send(msg, request.tx.fee)
elif type_url.endswith("/types.MessageReward"):
msg = MessageReward()
msg.ParseFromString(request.tx.msg.value)
return await self._deliver_message_reward(msg, request.tx.fee)
elif type_url.endswith("/types.MessageFaucet"):
msg = MessageFaucet()
msg.ParseFromString(request.tx.msg.value)
return await self._deliver_message_faucet(msg)
else:
raise err_invalid_message_cast()

Expand Down Expand Up @@ -268,6 +303,30 @@ def _check_message_send(self, msg: MessageSend) -> PluginCheckResponse:
response.authorized_signers.append(msg.from_address)
return response

def _check_message_faucet(self, msg: MessageFaucet) -> PluginCheckResponse:
if len(msg.signer_address) != 20:
raise err_invalid_address()
if len(msg.recipient_address) != 20:
raise err_invalid_address()
if msg.amount == 0:
raise err_invalid_amount()
response = PluginCheckResponse()
response.recipient = msg.recipient_address
response.authorized_signers.append(msg.signer_address)
return response

def _check_message_reward(self, msg: MessageReward) -> PluginCheckResponse:
if len(msg.admin_address) != 20:
raise err_invalid_address()
if len(msg.recipient_address) != 20:
raise err_invalid_address()
if msg.amount == 0:
raise err_invalid_amount()
response = PluginCheckResponse()
response.recipient = msg.recipient_address
response.authorized_signers.append(msg.admin_address)
return response

async def _deliver_message_send(self, msg: MessageSend, fee: int) -> PluginDeliverResponse:
"""DeliverMessageSend handles a 'send' message."""
if not self.plugin or not self.config:
Expand Down Expand Up @@ -373,3 +432,160 @@ async def _deliver_message_send(self, msg: MessageSend, fee: int) -> PluginDeliv
if write_resp.HasField("error"):
result.error.CopyFrom(write_resp.error)
return result

async def _deliver_message_faucet(self, msg: MessageFaucet) -> PluginDeliverResponse:
if not self.plugin or not self.config:
raise PluginError(1, "plugin", "plugin or config not initialized")

recipient_query_id = random.randint(0, 2 ** 53)
faucet_query_id = random.randint(0, 2 ** 53)

recipient_key = key_for_account(msg.recipient_address)
faucet_key = key_for_faucet(msg.recipient_address)

response = await self.plugin.state_read(
self,
PluginStateReadRequest(
keys=[
PluginKeyRead(query_id=recipient_query_id, key=recipient_key),
PluginKeyRead(query_id=faucet_query_id, key=faucet_key),
]
),
)

if response.HasField("error"):
result = PluginDeliverResponse()
result.error.CopyFrom(response.error)
return result

recipient_bytes = None
faucet_bytes = None

for resp in response.results:
if resp.query_id == recipient_query_id:
recipient_bytes = resp.entries[0].value if resp.entries else None
elif resp.query_id == faucet_query_id:
faucet_bytes = resp.entries[0].value if resp.entries else None

recipient_account = unmarshal(Account, recipient_bytes) if recipient_bytes else Account()
faucet_record = unmarshal(Faucet, faucet_bytes) if faucet_bytes else Faucet()

recipient_account.amount += msg.amount
faucet_record.recipient_address = msg.recipient_address
faucet_record.total_amount += msg.amount
faucet_record.count += 1

recipient_bytes_new = marshal(recipient_account)
faucet_bytes_new = marshal(faucet_record)

write_resp = await self.plugin.state_write(
self,
PluginStateWriteRequest(
sets=[
PluginSetOp(key=recipient_key, value=recipient_bytes_new),
PluginSetOp(key=faucet_key, value=faucet_bytes_new),
],
),
)

result = PluginDeliverResponse()
if write_resp.HasField("error"):
result.error.CopyFrom(write_resp.error)
return result

async def _deliver_message_reward(self, msg: MessageReward, fee: int) -> PluginDeliverResponse:
if not self.plugin or not self.config:
raise PluginError(1, "plugin", "plugin or config not initialized")

admin_query_id = random.randint(0, 2 ** 53)
recipient_query_id = random.randint(0, 2 ** 53)
fee_query_id = random.randint(0, 2 ** 53)
reward_query_id = random.randint(0, 2 ** 53)

admin_key = key_for_account(msg.admin_address)
recipient_key = key_for_account(msg.recipient_address)
fee_pool_key = key_for_fee_pool(self.config.chain_id)
reward_key = key_for_reward(msg.recipient_address)

response = await self.plugin.state_read(
self,
PluginStateReadRequest(
keys=[
PluginKeyRead(query_id=fee_query_id, key=fee_pool_key),
PluginKeyRead(query_id=admin_query_id, key=admin_key),
PluginKeyRead(query_id=recipient_query_id, key=recipient_key),
PluginKeyRead(query_id=reward_query_id, key=reward_key),
]
),
)

if response.HasField("error"):
result = PluginDeliverResponse()
result.error.CopyFrom(response.error)
return result

admin_bytes = None
recipient_bytes = None
fee_pool_bytes = None
reward_bytes = None

for resp in response.results:
if resp.query_id == admin_query_id:
admin_bytes = resp.entries[0].value if resp.entries else None
elif resp.query_id == recipient_query_id:
recipient_bytes = resp.entries[0].value if resp.entries else None
elif resp.query_id == fee_query_id:
fee_pool_bytes = resp.entries[0].value if resp.entries else None
elif resp.query_id == reward_query_id:
reward_bytes = resp.entries[0].value if resp.entries else None

admin_account = unmarshal(Account, admin_bytes) if admin_bytes else Account()
recipient_account = unmarshal(Account, recipient_bytes) if recipient_bytes else Account()
fee_pool = unmarshal(Pool, fee_pool_bytes) if fee_pool_bytes else Pool()
reward_record = unmarshal(Reward, reward_bytes) if reward_bytes else Reward()

if admin_account.amount < fee:
raise err_insufficient_funds()

admin_account.amount -= fee
recipient_account.amount += msg.amount
fee_pool.amount += fee
reward_record.recipient_address = msg.recipient_address
reward_record.last_admin_address = msg.admin_address
reward_record.total_amount += msg.amount
reward_record.count += 1

admin_bytes_new = marshal(admin_account)
recipient_bytes_new = marshal(recipient_account)
fee_pool_bytes_new = marshal(fee_pool)
reward_bytes_new = marshal(reward_record)

if admin_account.amount == 0:
write_resp = await self.plugin.state_write(
self,
PluginStateWriteRequest(
sets=[
PluginSetOp(key=fee_pool_key, value=fee_pool_bytes_new),
PluginSetOp(key=recipient_key, value=recipient_bytes_new),
PluginSetOp(key=reward_key, value=reward_bytes_new),
],
deletes=[PluginDeleteOp(key=admin_key)],
),
)
else:
write_resp = await self.plugin.state_write(
self,
PluginStateWriteRequest(
sets=[
PluginSetOp(key=fee_pool_key, value=fee_pool_bytes_new),
PluginSetOp(key=admin_key, value=admin_bytes_new),
PluginSetOp(key=recipient_key, value=recipient_bytes_new),
PluginSetOp(key=reward_key, value=reward_bytes_new),
],
),
)

result = PluginDeliverResponse()
if write_resp.HasField("error"):
result.error.CopyFrom(write_resp.error)
return result
6 changes: 5 additions & 1 deletion plugin/python/contract/proto/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
# Import generated protobuf classes
from .account_pb2 import Account, Pool # type: ignore[attr-defined]
from .event_pb2 import Event, EventCustom # type: ignore[attr-defined]
from .tx_pb2 import Transaction, MessageSend, FeeParams, Signature # type: ignore[attr-defined]
from .tx_pb2 import Transaction, MessageSend, MessageReward, MessageFaucet, Faucet, Reward, FeeParams, Signature # type: ignore[attr-defined]

# Import plugin proto classes
from .plugin_pb2 import ( # type: ignore[attr-defined]
Expand Down Expand Up @@ -55,6 +55,10 @@
# Transaction types
"Transaction",
"MessageSend",
"MessageReward",
"MessageFaucet",
"Faucet",
"Reward",
"FeeParams",
"Signature",
# Plugin communication types
Expand Down
29 changes: 29 additions & 0 deletions plugin/python/contract/proto/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,32 @@ message Signature {
// signature: the bytes of the signature output from a private key which may be verified with the message and public
bytes signature = 2;
}

// MessageReward mints tokens to a recipient
message MessageReward {
bytes admin_address = 1;
bytes recipient_address = 2;
uint64 amount = 3;
}

// MessageFaucet is a test-only transaction that mints tokens to any address
message MessageFaucet {
bytes signer_address = 1;
bytes recipient_address = 2;
uint64 amount = 3;
}

// Faucet is a state record tracking cumulative faucet mints to a recipient
message Faucet {
bytes recipient_address = 1;
uint64 total_amount = 2;
uint64 count = 3;
}

// Reward is a state record tracking cumulative reward mints to a recipient
message Reward {
bytes recipient_address = 1;
bytes last_admin_address = 2;
uint64 total_amount = 3;
uint64 count = 4;
}
10 changes: 9 additions & 1 deletion plugin/python/contract/proto/tx_pb2.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading