Skip to content
Merged
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
commands; kind, source, receiver, amount, fee and memo of a sent command.
- `AccountNotFoundError`, a `ValueError`, from `get_account`.
- `execute_query`, for custom GraphQL documents, as in the other SDKs.
- ITN methods for harness support, which need a daemon with
MinaProtocol/mina#19616: `commit_id`, `scheduled_transactions`,
`schedule_payments_with_handle`, `schedule_zkapp_commands_with_handle` and
`create_accounts` (`CreateAccountsDetails`, `CreatedAccounts`). Live tests
run with `MINA_ITN_HARNESS=1`, and `create_accounts` also needs
`MINA_ITN_FEE_PAYER`. `spec/` is mina-sdk-spec v0.2.0.
- ITN client, `mina_sdk.itn` (extra `itn`, which adds `cryptography`):
`ItnClient` for the daemon's ITN GraphQL server (`--itn-graphql-port`),
with ed25519 request signing (`ItnKey`), the `auth` handshake, sequence
Expand Down
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,21 @@ with ItnClient("http://127.0.0.1:3086/graphql", key) as itn:
| `set_zkapp_command_limit(limit)` | `zkAppCommandLimit` |
| `execute_query(query, variables, query_name)` | any document, sequenced and signed |

These need a daemon with MinaProtocol/mina#19616; older daemons answer them
with a GraphQL error:

| Method | GraphQL |
|--------|---------|
| `commit_id()` | `auth { commitId }`: the daemon's git commit |
| `scheduled_transactions()` | `scheduledTransactions`: handles of the running schedulers |
| `schedule_payments_with_handle(PaymentsDetails, handle)` | `schedulePayments` with a caller-chosen handle |
| `schedule_zkapp_commands_with_handle(ZkappCommandsDetails, handle)` | `scheduleZkappCommands` with a caller-chosen handle |
| `create_accounts(CreateAccountsDetails, handle=None)` | `createAccounts`: keys at once, funding in the background under the handle |

A handle is a UUID that the caller chooses and records before the call. A
call with the handle of a running scheduler starts nothing and returns that
handle, so these calls may be repeated after a transport error.

Requests of one client are sent one at a time, because the daemon accepts
only its exact next sequence number; the client is safe to share between
threads. A sequenced request is never repeated after a transport error,
Expand Down
30 changes: 30 additions & 0 deletions spec/ITN.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,36 @@ A client must:
| `StopDaemon` | `stop_daemon` | `StopDaemon` | `stopDaemon` | `stop_daemon` | [delay seconds], [clean config] | string |
| `ZkappCommandLimit` | `set_zkapp_command_limit` | `SetZkappCommandLimit` | `setZkappCommandLimit` | `set_zkapp_command_limit` | limit or null | limit now in force |

### Operations for harness support

These operations need a daemon with MinaProtocol/mina#19616. Older daemons
reject them, so a client uses them only against such daemons; the operations
above do not change. A client can call `CommitId` first: an older daemon
answers it with a GraphQL error.

| Operation | Rust | Go | JS | Python | Arguments | Returns |
|:--|:--|:--|:--|:--|:--|:--|
| `CommitId` | `commit_id` | `CommitID` | `commitId` | `commit_id` | – | the daemon's git commit |
| `ScheduledTransactions` | `scheduled_transactions` | `ScheduledTransactions` | `scheduledTransactions` | `scheduled_transactions` | – | handles of the running schedulers |
| `SchedulePaymentsWithHandle` | `schedule_payments_with_handle` | `SchedulePaymentsWithHandle` | `schedulePaymentsWithHandle` | `schedule_payments_with_handle` | `PaymentsDetails`, handle | handle |
| `ScheduleZkappCommandsWithHandle` | `schedule_zkapp_commands_with_handle` | `ScheduleZkappCommandsWithHandle` | `scheduleZkappCommandsWithHandle` | `schedule_zkapp_commands_with_handle` | `ZkappCommandsDetails`, handle | handle |
| `CreateAccounts` | `create_accounts` | `CreateAccounts` | `createAccounts` | `create_accounts` | `CreateAccountsDetails`, [handle] | handle and the new accounts (public and private key) |

- **Handles.** A handle is a UUID that the client chooses. The client records
it before it sends the request, so that it can find the scheduler again
after a lost response or a restart. A request with the handle of a running
scheduler starts nothing and returns that handle; so a repeat after a
transport error is safe for these mutations, unlike the others.
- **`ScheduledTransactions`** lists the handles of the running payment and
zkApp schedulers and account-creation jobs. A client that lost its own
record can stop each of them with `StopScheduledTransactions`.
- **`CreateAccounts`** replaces `mina advanced itn-create-accounts`, which
talks to the daemon over its bin_prot RPC. It returns the keys at once and
funds the accounts in the background under the handle: a client waits until
`ScheduledTransactions` no longer lists the handle. The amount is divided
among the accounts, and each account pays the account creation fee out of
its share.

The input types `PaymentsDetails`, `ZkappCommandsDetails` and `GatingUpdate`
have the fields of [`schema/itn.graphql`](https://github.com/o1-labs/mina-sdk-spec/blob/main/schema/itn.graphql). Every SDK also
has a custom-query method that signs and sequences any document.
Expand Down
2 changes: 1 addition & 1 deletion spec/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v0.1.2
v0.2.0-rc.1
33 changes: 33 additions & 0 deletions spec/itn-operations.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,36 @@ mutation StopDaemon($delaySeconds: Int, $cleanConfig: Boolean) {
mutation ZkappCommandLimit($limit: Int) {
zkAppCommandLimit(limit: $limit)
}

# The following operations need a daemon with MinaProtocol/mina's ITN harness
# support (caller-supplied handles, createAccounts, scheduledTransactions,
# commitId). Older daemons reject them, so clients use them only against such
# daemons; the operations above are unchanged.

query CommitId {
auth {
commitId
}
}

query ScheduledTransactions {
scheduledTransactions
}

mutation SchedulePaymentsWithHandle($input: PaymentsDetails!, $handle: String!) {
schedulePayments(input: $input, handle: $handle)
}

mutation ScheduleZkappCommandsWithHandle($input: ZkappCommandsDetails!, $handle: String!) {
scheduleZkappCommands(input: $input, handle: $handle)
}

mutation CreateAccounts($input: CreateAccountsDetails!, $handle: String) {
createAccounts(input: $input, handle: $handle) {
handle
accounts {
publicKey
privateKey
}
}
}
6 changes: 6 additions & 0 deletions src/mina_sdk/itn/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
)
from mina_sdk.itn.key import ItnKey
from mina_sdk.itn.types import (
CreateAccountsDetails,
CreatedAccount,
CreatedAccounts,
GatingUpdate,
ItnAuth,
ItnLog,
Expand All @@ -30,6 +33,9 @@
)

__all__ = [
"CreateAccountsDetails",
"CreatedAccount",
"CreatedAccounts",
"GatingUpdate",
"InvalidItnKeyError",
"ItnAuth",
Expand Down
62 changes: 62 additions & 0 deletions src/mina_sdk/itn/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@
from mina_sdk.itn.errors import ItnHttpError, ItnSequencingError, ItnUnauthorizedError
from mina_sdk.itn.key import ItnKey
from mina_sdk.itn.types import (
CreateAccountsDetails,
CreatedAccount,
CreatedAccounts,
GatingUpdate,
ItnAuth,
ItnLog,
Expand Down Expand Up @@ -319,6 +322,65 @@ def set_zkapp_command_limit(self, limit: int | None) -> int | None:
)
return data["zkAppCommandLimit"]

# The following methods need a daemon with MinaProtocol/mina#19616; older
# daemons answer them with a GraphQL error. A handle is a UUID that the
# caller chooses and records before the call. A call with the handle of a
# running scheduler starts nothing and returns that handle, so these calls
# may be repeated after a transport error.

def commit_id(self) -> str:
"""The git commit of the daemon's build."""
data = self.execute_query(queries.COMMIT_ID, None, "itn_commit_id")
return data["auth"]["commitId"]

def scheduled_transactions(self) -> list[str]:
"""Handles of the running payment and zkApp schedulers and account-creation jobs."""
data = self.execute_query(
queries.SCHEDULED_TRANSACTIONS, None, "itn_scheduled_transactions"
)
return list(data["scheduledTransactions"])

def schedule_payments_with_handle(self, details: PaymentsDetails, handle: str) -> str:
"""Start sending payments under ``handle``; returns it."""
data = self.execute_query(
queries.SCHEDULE_PAYMENTS_WITH_HANDLE,
{"input": details.to_variables(), "handle": handle},
"itn_schedule_payments_with_handle",
)
return data["schedulePayments"]

def schedule_zkapp_commands_with_handle(
self, details: ZkappCommandsDetails, handle: str
) -> str:
"""Start sending zkApp commands under ``handle``; returns it."""
data = self.execute_query(
queries.SCHEDULE_ZKAPP_COMMANDS_WITH_HANDLE,
{"input": details.to_variables(), "handle": handle},
"itn_schedule_zkapp_commands_with_handle",
)
return data["scheduleZkappCommands"]

def create_accounts(
self, details: CreateAccountsDetails, handle: str | None = None
) -> CreatedAccounts:
"""Create ``details.num_accounts`` accounts and fund them in the
background; the keys are returned at once. Wait until
``scheduled_transactions`` no longer lists the returned handle. ``None``
lets the daemon choose the handle."""
data = self.execute_query(
queries.CREATE_ACCOUNTS,
{"input": details.to_variables(), "handle": handle},
"itn_create_accounts",
)
created = data["createAccounts"]
return CreatedAccounts(
handle=created["handle"],
accounts=[
CreatedAccount(public_key=a["publicKey"], private_key=a["privateKey"])
for a in created["accounts"]
],
)


def _parse_auth(data: dict[str, Any]) -> ItnAuth:
auth = data.get("auth")
Expand Down
36 changes: 36 additions & 0 deletions src/mina_sdk/itn/queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,37 @@
zkAppCommandLimit(limit: $limit)
}"""

# The following operations need a daemon with MinaProtocol/mina#19616; older
# daemons answer them with a GraphQL error.

COMMIT_ID = """query CommitId {
auth {
commitId
}
}"""

SCHEDULED_TRANSACTIONS = """query ScheduledTransactions {
scheduledTransactions
}"""

SCHEDULE_PAYMENTS_WITH_HANDLE = """mutation SchedulePaymentsWithHandle($input: PaymentsDetails!, $handle: String!) {
schedulePayments(input: $input, handle: $handle)
}"""

SCHEDULE_ZKAPP_COMMANDS_WITH_HANDLE = """mutation ScheduleZkappCommandsWithHandle($input: ZkappCommandsDetails!, $handle: String!) {
scheduleZkappCommands(input: $input, handle: $handle)
}"""

CREATE_ACCOUNTS = """mutation CreateAccounts($input: CreateAccountsDetails!, $handle: String) {
createAccounts(input: $input, handle: $handle) {
handle
accounts {
publicKey
privateKey
}
}
}"""

# Every ITN document of the specification, for the conformance test.
ALL_ITN_DOCUMENTS = [
AUTH,
Expand All @@ -73,4 +104,9 @@
UPDATE_GATING,
STOP_DAEMON,
ZKAPP_COMMAND_LIMIT,
COMMIT_ID,
SCHEDULED_TRANSACTIONS,
SCHEDULE_PAYMENTS_WITH_HANDLE,
SCHEDULE_ZKAPP_COMMANDS_WITH_HANDLE,
CREATE_ACCOUNTS,
]
43 changes: 43 additions & 0 deletions src/mina_sdk/itn/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,3 +199,46 @@ def to_variables(self) -> dict[str, Any]:
"bannedPeers": [p.to_variables() for p in self.banned_peers],
"trustedPeers": [p.to_variables() for p in self.trusted_peers],
}


@dataclass(frozen=True)
class CreateAccountsDetails:
"""The input of ``create_accounts``.

Attributes:
fee_payer: Base58 private key of the account that funds the new accounts.
num_accounts: Number of new accounts.
fee: Fee of each zkApp command that creates accounts.
amount: Divided among the new accounts; each pays the account creation
fee out of its share.
"""

fee_payer: str
num_accounts: int
fee: Currency
amount: Currency

def to_variables(self) -> dict[str, Any]:
return {
"feePayer": self.fee_payer,
"numAccounts": self.num_accounts,
"fee": self.fee.to_nanomina_str(),
"amount": self.amount.to_nanomina_str(),
}


@dataclass(frozen=True)
class CreatedAccount:
"""A new account of ``create_accounts``: public and base58 private key."""

public_key: str
private_key: str


@dataclass(frozen=True)
class CreatedAccounts:
"""The result of ``create_accounts``. ``scheduled_transactions`` lists the
handle until the background job that funds the accounts ends."""

handle: str
accounts: list[CreatedAccount]
59 changes: 59 additions & 0 deletions tests/test_itn.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@

from mina_sdk import Currency, DaemonConnectionError, GraphQLError
from mina_sdk.itn import (
CreateAccountsDetails,
CreatedAccount,
GatingUpdate,
InvalidItnKeyError,
ItnClient,
Expand Down Expand Up @@ -272,3 +274,60 @@ def test_variables_of_the_operations(key):
assert variables[4]["input"]["trustedPeers"] == [
{"host": "1.2.3.4", "libp2pPort": 1, "peerId": "p"}
]


@respx.mock
def test_harness_support_operations(key):
def answer(query):
if "commitId" in query:
return {"auth": {"commitId": "abc123"}}
if "scheduledTransactions" in query:
return {"scheduledTransactions": ["h1", "h2"]}
if "createAccounts" in query:
return {
"createAccounts": {
"handle": "h3",
"accounts": [{"publicKey": "B62qa", "privateKey": "EKa"}],
}
}
return {"schedulePayments": "h4"}

server = MockItnServer(key)
data_for = server.data

def respond(request):
server.data = answer(json.loads(request.content)["query"])
return server(request)

respx.post(URL).mock(side_effect=respond)
assert data_for == {}
details = CreateAccountsDetails(
fee_payer="EKfee",
num_accounts=2,
fee=Currency.from_nanomina(100),
amount=Currency.from_nanomina(5000),
)
payments = PaymentsDetails(
duration_min=1,
tps=0.5,
memo_prefix="m",
max_fee=Currency.from_nanomina(20),
min_fee=Currency.from_nanomina(10),
amount=Currency.from_nanomina(1),
receiver="B62qr",
)
with _client(key) as itn:
assert itn.commit_id() == "abc123"
assert itn.scheduled_transactions() == ["h1", "h2"]
created = itn.create_accounts(details)
assert created.handle == "h3"
assert created.accounts == [CreatedAccount(public_key="B62qa", private_key="EKa")]
itn.create_accounts(details, "u1")
assert itn.schedule_payments_with_handle(payments, "u2") == "h4"
variables = [r.get("variables") for r in server.requests if r["query"] != queries.AUTH]
assert variables[2] == {
"input": {"feePayer": "EKfee", "numAccounts": 2, "fee": "100", "amount": "5000"},
"handle": None,
}
assert variables[3]["handle"] == "u1"
assert variables[4]["handle"] == "u2"
Loading
Loading