Skip to content

Commit

Permalink
Add function annotations 🚀 (hyperledger#1557)
Browse files Browse the repository at this point in the history
  • Loading branch information
LucasSte committed Oct 13, 2023
1 parent 817831a commit 2c29fd7
Show file tree
Hide file tree
Showing 82 changed files with 1,407 additions and 961 deletions.
18 changes: 18 additions & 0 deletions docs/examples/solana/account_access.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@

contract Foo {
@account(oneAccount)
@signer(mySigner)
@mutableAccount(otherAccount)
@mutableSigner(otherSigner)
function bar() external returns (uint64) {
assert(tx.accounts.mySigner.is_signer);
assert(tx.accounts.otherSigner.is_signer);
assert(tx.accounts.otherSigner.is_writable);
assert(tx.accounts.otherAccount.is_writable);

tx.accounts.otherAccount.data[0] = 0xca;
tx.accounts.otherSigner.data[1] = 0xfe;

return tx.accounts.oneAccount.lamports;
}
}
15 changes: 9 additions & 6 deletions docs/examples/solana/create_contract_with_metas.sol
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import 'solana';

contract creator {
function create_with_metas(address data_account_to_initialize, address payer) public {

@mutableSigner(data_account_to_initialize)
@mutableSigner(payer)
function create_with_metas() external {
AccountMeta[3] metas = [
AccountMeta({
pubkey: data_account_to_initialize,
pubkey: tx.accounts.data_account_to_initialize.key,
is_signer: true,
is_writable: true}),
AccountMeta({
pubkey: payer,
pubkey: tx.accounts.payer.key,
is_signer: true,
is_writable: true}),
AccountMeta({
Expand All @@ -17,16 +20,16 @@ contract creator {
is_signer: false})
];

Child.new{accounts: metas}(payer);
Child.new{accounts: metas}();

Child.use_metas();
Child.use_metas{accounts: []}();
}
}

@program_id("Chi1d5XD6nTAp2EyaNGqMxZzUjh6NvhXRxbGHP3D1RaT")
contract Child {
@payer(payer)
constructor(address payer) {
constructor() {
print("In child constructor");
}

Expand Down
2 changes: 1 addition & 1 deletion docs/examples/solana/expression_this_external_call.sol
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
@program_id("H3AthiA2C1pcMahg17nEwqr9628gkXUnnzWJJ3iSDekL")
contract kadowari {
function nomi() public {
this.nokogiri(102);
this.nokogiri{accounts: []}(102);
}

function nokogiri(int256 a) public {
Expand Down
2 changes: 1 addition & 1 deletion docs/examples/solana/function_call.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ contract A {
uint32(5)
);

(bool success, bytes rawresult) = v.call(data);
(bool success, bytes rawresult) = v.call{accounts: []}(data);

assert(success == true);

Expand Down
4 changes: 2 additions & 2 deletions docs/examples/solana/function_call_external.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ contract foo {

contract bar {
function test(address f) public {
(address f1, bytes32 f2) = foo.bar1{program_id: f}(102, false);
bool f3 = foo.bar2{program_id: f}({x: 255, y: true});
(address f1, bytes32 f2) = foo.bar1{program_id: f, accounts: []}(102, false);
bool f3 = foo.bar2{program_id: f, accounts: []}({x: 255, y: true});
}
}
4 changes: 2 additions & 2 deletions docs/examples/solana/function_type_callback.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
contract ft {
function test(address p) public {
function test(address p) external {
// this.callback can be used as an external function type value
paffling.set_callback{program_id: p}(this.callback);
}
Expand All @@ -19,6 +19,6 @@ contract paffling {
}

function piffle() public {
callback(1, "paffled");
callback{accounts: []}(1, "paffled");
}
}
4 changes: 4 additions & 0 deletions docs/examples/solana/payer_annotation.sol
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ contract Builder {
})
];
BeingBuilt.new{accounts: metas}("my_seed");


// No accounts are needed in this call, so we pass an empty vector.
BeingBuilt.say_this{accounts: []}("It's summertime!");
}
}

Expand Down
2 changes: 1 addition & 1 deletion docs/examples/solana/program_id.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ contract Bar {
}

function call_foo() public {
Foo.say_hello();
Foo.say_hello{accounts: []}();
}

function foo_at_another_address(address other_foo_id) external {
Expand Down
22 changes: 6 additions & 16 deletions docs/examples/solana/use_authority.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,19 @@ contract AuthorityExample {
address authority;
uint64 counter;

modifier needs_authority() {
for (uint64 i = 0; i < tx.accounts.length; i++) {
AccountInfo ai = tx.accounts[i];

if (ai.key == authority && ai.is_signer) {
_;
return;
}
}

print("not signed by authority");
revert();
}

constructor(address initial_authority) {
authority = initial_authority;
}

function set_new_authority(address new_authority) needs_authority public {
@signer(authorityAccount)
function set_new_authority(address new_authority) external {
assert(tx.accounts.authorityAccount.key == authority && tx.accounts.authorityAccount.is_signer);
authority = new_authority;
}

function inc() needs_authority public {
@signer(authorityAccount)
function inc() external {
assert(tx.accounts.authorityAccount.key == authority && tx.accounts.authorityAccount.is_signer);
counter += 1;
}

Expand Down
10 changes: 10 additions & 0 deletions docs/language/functions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,16 @@ calling.
(bool success, bytes rawresult) = foo.call{value: 102, gas: 1000}(rawcalldata);
}
External calls with the ``call()`` method on Solana must have the ``accounts`` call argument, regardless of the
callee function visibility, because the compiler has no information about the caller function to generate the
``AccountMeta`` array automatically.

.. code-block:: solidity
function test(address foo, bytes rawcalldata) public {
(bool success, bytes rawresult) = foo.call{accounts: []}(rawcalldata);
}
.. _fallback_receive:

Calling an external function using ``delegatecall``
Expand Down
10 changes: 10 additions & 0 deletions docs/language/types.rst
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,16 @@ a function on particular contract instance.
:code: solidity


On Solana, external calls from variables of type external functions require the ``accounts`` call argument. The
compiler cannot determine the accounts such a function needs, so it does not automatically generate the
``AccountsMeta`` array.

.. code-block:: solidity
function test(function(int32, string) external myFunc) public {
myFunc{accounts: []}(24, "accounts");
}
Storage References
__________________

Expand Down
43 changes: 30 additions & 13 deletions docs/targets/solana.rst
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,11 @@ Runtime
- Function selectors are eight bytes wide and known as *discriminators*.
- Solana provides different builtins, e.g. ``block.slot`` and ``tx.accounts``.
- When calling an external function or invoking a contract's constructor, one
:ref:`needs to provide <solana_constructor>` the necessary accounts for the transaction.
:ref:`needs to provide <solana_cpi_accounts>` the necessary accounts for the transaction.
- The keyword ``this`` returns the contract's program account, also know as program id.
- Contracts :ref:`cannot be types <contracts_not_types>` on Solana and :ref:`calls to contracts <solana_contract_call>`
follow a different syntax.
- Accounts can be declared on functions using :ref:`annotations <account_management>`.


Compute budget
Expand Down Expand Up @@ -399,24 +400,40 @@ an argument. This annotation defines a Solana account that is going to pay for t
account. The syntax ``@payer(my_account)`` declares an account named ``my_account``, which will be
required for every call to the constructor.

Similarly, for external functions in a contract, one can declare the necessary accounts, using function annotations.
``@account(myAcc)`` declares a read only account ``myAcc``, while ``@mutableAccount(otherAcc)`` declares a mutable
account ``otherAcc``. For signer accounts, the annotations follow the syntax ``@signer(mySigner)`` and
``@mutableSigner(myOtherSigner)``.


Accessing accounts' data
++++++++++++++++++++++++

Accounts declared on a constructor using the ``@payer`` annotation are available for access inside it. Likewise,
accounts declared on external functions with any of the aforementioned annotations are also available in the
``tx.accounts`` vector for easy access. For an account declared as ``@account(funder)``, the access follows the
syntax ``tx.accounts.funder``, which returns the :ref:`AccountInfo builtin struct <account_info>`.

.. include:: ../examples/solana/account_access.sol
:code: solidity

.. _solana_cpi_accounts:

External calls with accounts
++++++++++++++++++++++++++++

In any Solana cross program invocation, including constructor calls, all the accounts a transaction needs must be
informed. Whenever possible, the compiler will automatically generate the ``AccountMeta`` array that satisfies
this requirement. Currently, that only works if the constructor call is done in an function declared external, as shown
in the example below. In any other case, the ``AccountMeta`` array must be manually created, following an account ordering
the IDL file specifies.
informed. If the ``{accounts: ...}`` call argument is missing from an external call, the compiler will automatically
generate the ``AccountMeta`` array that satisfies such a requirement. Currently, that only works if the call is done
in an function declared external, as shown in the example below. In any other case, the ``AccountMeta`` array must
be manually created, following the account ordering the IDL file specifies. If a certain call does not need any accounts,
an empty vector must be passed ``{accounts: []}``.

The following example shows two correct ways of calling a constructor. Note that the IDL for the ``BeingBuilt`` contract
The following example shows two correct ways of calling a contract. Note that the IDL for the ``BeingBuilt`` contract
has an instruction called ``new``, representing the contract's constructor, whose accounts are specified in the
following order: ``dataAccount``, ``payer_account``, ``systemAccount``. That is the order one must follow when invoking
such a constructor.

.. include:: ../examples/solana/payer_annotation.sol
:code: solidity


Accessing accounts' data
________________________

Accounts declared on a constructor using the ``@payer`` annotation are available for access inside it.
For an account declared as ``@payer(funder)``, the access follows the syntax ``tx.accounts.funder``, which returns
the :ref:`AccountInfo builtin struct <account_info>`.
10 changes: 6 additions & 4 deletions integration/solana/account_data.sol
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import '../../solana-library/spl_token.sol';

contract AccountData {
function token_account(address addr) view public returns (SplToken.TokenAccountData) {
return SplToken.get_token_account_data(addr);
@account(addr)
function token_account() view external returns (SplToken.TokenAccountData) {
return SplToken.get_token_account_data(tx.accounts.addr);
}

function mint_account(address addr) view public returns (SplToken.MintAccountData) {
return SplToken.get_mint_account_data(addr);
@account(addr)
function mint_account() view external returns (SplToken.MintAccountData) {
return SplToken.get_mint_account_data(tx.accounts.addr);
}
}
54 changes: 12 additions & 42 deletions integration/solana/account_data.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,8 @@ describe('Deserialize account data', function () {
owner.publicKey
);

let res = await program.methods.tokenAccount(token_account.address)
.accounts({dataAccount: storage.publicKey})
.remainingAccounts(
[
{pubkey: token_account.address, isSigner: false, isWritable: false}
]
)
let res = await program.methods.tokenAccount()
.accounts({addr: token_account.address})
.view();

expect(res.mintAccount).toEqual(token_account.mint);
Expand Down Expand Up @@ -86,13 +81,8 @@ describe('Deserialize account data', function () {
);
token_account = await getAccount(connection, token_account.address);

res = await program.methods.tokenAccount(token_account.address)
.accounts({dataAccount: storage.publicKey})
.remainingAccounts(
[
{pubkey: token_account.address, isSigner: false, isWritable: false}
]
)
res = await program.methods.tokenAccount()
.accounts({addr: token_account.address})
.view();

// The delegate account should be present now
Expand All @@ -112,13 +102,8 @@ describe('Deserialize account data', function () {
);
token_account = await getAccount(connection, token_account.address);

res = await program.methods.tokenAccount(token_account.address)
.accounts({dataAccount: storage.publicKey})
.remainingAccounts(
[
{pubkey: token_account.address, isSigner: false, isWritable: false}
]
)
res = await program.methods.tokenAccount()
.accounts({addr: token_account.address})
.view();

// The close authority should be present
Expand All @@ -134,13 +119,8 @@ describe('Deserialize account data', function () {
owner.publicKey
);

res = await program.methods.tokenAccount(token_account.address)
.accounts({dataAccount: storage.publicKey})
.remainingAccounts(
[
{pubkey: token_account.address, isSigner: false, isWritable: false}
]
)
res = await program.methods.tokenAccount()
.accounts({addr: token_account.address})
.view();

// Is native must be present
Expand Down Expand Up @@ -182,13 +162,8 @@ describe('Deserialize account data', function () {

let mint_data = await getMint(connection, mint);

let res = await program.methods.mintAccount(mint)
.accounts({dataAccount: storage.publicKey})
.remainingAccounts(
[
{pubkey: mint, isWritable: false, isSigner: false}
]
)
let res = await program.methods.mintAccount()
.accounts({addr: mint})
.view();

// Authorities are present
Expand Down Expand Up @@ -222,13 +197,8 @@ describe('Deserialize account data', function () {

mint_data = await getMint(connection, mint);

res = await program.methods.mintAccount(mint)
.accounts({dataAccount: storage.publicKey})
.remainingAccounts(
[
{pubkey: mint, isWritable: false, isSigner: false}
]
)
res = await program.methods.mintAccount()
.accounts({addr: mint})
.view();

// Authorities are not present
Expand Down
Loading

0 comments on commit 2c29fd7

Please sign in to comment.