Skip to content

Commit

Permalink
Implement system program nonce account methods.
Browse files Browse the repository at this point in the history
  • Loading branch information
jpe7s committed Feb 1, 2025
1 parent b0bc4da commit 4ec0d2b
Show file tree
Hide file tree
Showing 5 changed files with 307 additions and 8 deletions.
99 changes: 93 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,16 @@
- [sava-core](https://github.com/sava-software/sava)
- [sava-rpc](https://github.com/sava-software/sava)

### Add Dependency
## Dependency Configuration

Create
a [GitHub user access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-personal-access-token-classic)
with read access to GitHub Packages.
### GitHub Access Token

Then add the following to your Gradle build script.
[Generate a classic token](https://github.com/settings/tokens) with the `read:packages` scope needed to access
dependencies hosted on GitHub Package Repository.

### Gradle

#### build.gradle

```groovy
repositories {
Expand All @@ -33,6 +36,10 @@ repositories {
}
maven {
url = "https://maven.pkg.github.com/sava-software/solana-programs"
credentials {
username = GITHUB_USERNAME
password = GITHUB_PERSONAL_ACCESS_TOKEN
}
}
}
Expand All @@ -45,4 +52,84 @@ dependencies {

## Contribution

Unit tests are needed and welcomed. Otherwise, please open an issue or send an email before working on a pull request.
Unit tests are needed and welcomed. Otherwise, please open a discussion, issue or send an email before working on a pull
request.

## Durable Transactions

### Create & Initialize Nonce Account

```
final Signer signer = ...
final var rpcEndpoint = URI.create("https://mainnet.helius-rpc.com/?api-key=");
try (final var httpClient = HttpClient.newHttpClient()) {
final var rpcClient = SolanaRpcClient.createClient(rpcEndpoint, httpClient);
final var blockHashFuture = rpcClient.getLatestBlockHash();
final var minRentFuture = rpcClient.getMinimumBalanceForRentExemption(NonceAccount.BYTES);
final var solanaAccounts = SolanaAccounts.MAIN_NET;
final var nonceAccountWithSeed = PublicKey.createOffCurveAccountWithAsciiSeed(
signer.publicKey(),
"nonce",
solanaAccounts.systemProgram()
);
final var initializeNonceAccountIx = SystemProgram.initializeNonceAccount(
solanaAccounts,
nonceAccountWithSeed.publicKey(),
signer.publicKey()
);
System.out.format("""
Fetching block hash and minimum rent to create nonce account %s with authority %s.
""",
nonceAccountWithSeed.publicKey(),
signer.publicKey()
);
final long minRent = minRentFuture.join();
final var createNonceAccountIx = SystemProgram.createAccountWithSeed(
solanaAccounts.invokedSystemProgram(),
signer.publicKey(),
nonceAccountWithSeed,
minRent,
NonceAccount.BYTES,
solanaAccounts.systemProgram()
);
final var instructions = List.of(createNonceAccountIx, initializeNonceAccountIx);
final var transaction = Transaction.createTx(signer.publicKey(), instructions);
final var blockHash = blockHashFuture.join().blockHash();
transaction.setRecentBlockHash(blockHash);
transaction.sign(signer);
final var base64Encoded = transaction.base64EncodeToString();
final var sendTransactionFuture = rpcClient.sendTransaction(base64Encoded);
System.out.format("""
Creating nonce account %s
https://explorer.solana.com/tx/%s
""",
nonceAccountWithSeed.publicKey(),
transaction.getBase58Id()
);
final var sig = sendTransactionFuture.join();
System.out.format("""
Confirmed transaction %s
https://solscan.io/account/%s
""",
sig,
nonceAccountWithSeed.publicKey()
);
final var nonceAccountInfo = rpcClient.getAccountInfo(nonceAccountWithSeed.publicKey()).join();
final var nonceAccount = NonceAccount.read(nonceAccountInfo);
System.out.println(nonceAccount);
}
```
2 changes: 1 addition & 1 deletion settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ dependencyResolutionManagement {
}
versionCatalogs {
libs {
from("software.sava:solana-version-catalog:0.4.106")
from("software.sava:solana-version-catalog:0.5.6")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package software.sava.solana.programs.system;

import software.sava.core.accounts.PublicKey;
import software.sava.core.encoding.ByteUtil;
import software.sava.core.rpc.Filter;
import software.sava.core.tx.Transaction;
import software.sava.rpc.json.http.response.AccountInfo;
import software.sava.solana.programs.stake.StakeState;

import java.util.function.BiFunction;

import static software.sava.core.accounts.PublicKey.PUBLIC_KEY_LENGTH;
import static software.sava.core.accounts.PublicKey.readPubKey;
import static software.sava.core.encoding.ByteUtil.getInt64LE;
import static software.sava.core.rpc.Filter.createDataSizeFilter;
import static software.sava.core.rpc.Filter.createMemCompFilter;

public record NonceAccount(PublicKey address,
int version,
State state,
PublicKey authority,
byte[] nonce,
long lamportsPerSignature) {

public enum State {
Uninitialized,
Initialized,
}

public static final int BYTES = 80;
public static final Filter DATA_SIZE_FILTER = createDataSizeFilter(BYTES);

public static final int VERSION_OFFSET = 0;
public static final int STATE_OFFSET = VERSION_OFFSET + Integer.BYTES;
public static final int AUTHORITY_OFFSET = STATE_OFFSET + Integer.BYTES;
public static final int NONCE_OFFSET = AUTHORITY_OFFSET + PUBLIC_KEY_LENGTH;
public static final int LAMPORTS_PER_SIG_OFFSET = NONCE_OFFSET + Transaction.BLOCK_HASH_LENGTH;

public static Filter createVersionFilter(final int version) {
final byte[] versionBytes = new byte[Integer.BYTES];
ByteUtil.putInt32LE(versionBytes, 0, version);
return createMemCompFilter(VERSION_OFFSET, versionBytes);
}

public static Filter createStateFilter(final StakeState state) {
final byte[] stateBytes = new byte[Integer.BYTES];
ByteUtil.putInt32LE(stateBytes, 0, state.ordinal());
return createMemCompFilter(STATE_OFFSET, stateBytes);
}

public static Filter createAuthorityFilter(final PublicKey authority) {
return createMemCompFilter(AUTHORITY_OFFSET, authority);
}

public static Filter createNonceFilter(final byte[] nonce) {
return createMemCompFilter(NONCE_OFFSET, nonce);
}

public static Filter createVersionFilter(final long lamportsPerSignature) {
final byte[] bytes = new byte[Long.BYTES];
ByteUtil.putInt64LE(bytes, 0, lamportsPerSignature);
return createMemCompFilter(VERSION_OFFSET, bytes);
}

public static NonceAccount read(final byte[] data, int offset) {
return read(null, data, offset);
}

public static NonceAccount read(final AccountInfo<byte[]> accountInfo) {
return read(accountInfo.pubKey(), accountInfo.data(), 0);
}

public static NonceAccount read(final PublicKey address, final byte[] data) {
return read(address, data, 0);
}

public static final BiFunction<PublicKey, byte[], NonceAccount> FACTORY = NonceAccount::read;

public static NonceAccount read(final PublicKey address, final byte[] data, int offset) {
final int version = ByteUtil.getInt32LE(data, offset);
offset += Integer.BYTES;
final var state = State.values()[ByteUtil.getInt32LE(data, offset)];
offset += Integer.BYTES;
final var authority = readPubKey(data, offset);
offset += PUBLIC_KEY_LENGTH;
final byte[] nonce = new byte[Transaction.BLOCK_HASH_LENGTH];
System.arraycopy(data, offset, nonce, 0, Transaction.BLOCK_HASH_LENGTH);
offset += Transaction.BLOCK_HASH_LENGTH;
final long lamportsPerSignature = getInt64LE(data, offset);
return new NonceAccount(
address,
version,
state,
authority,
nonce,
lamportsPerSignature
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import software.sava.core.accounts.AccountWithSeed;
import software.sava.core.accounts.PublicKey;
import software.sava.core.accounts.SolanaAccounts;
import software.sava.core.accounts.meta.AccountMeta;
import software.sava.core.programs.Discriminator;
import software.sava.core.tx.Instruction;
Expand Down Expand Up @@ -359,6 +360,98 @@ public static Instruction transferWithSeed(final AccountMeta invokedProgram,
return createInstruction(invokedProgram, keys, data);
}

public static Instruction advanceNonceAccount(final SolanaAccounts solanaAccounts,
final PublicKey nonceAccount,
final PublicKey nonceAuthority) {
final var keys = List.of(
createWrite(nonceAccount),
solanaAccounts.readRecentBlockhashesSysVar(),
createReadOnlySigner(nonceAuthority)
);

final byte[] data = new byte[NATIVE_DISCRIMINATOR_LENGTH];
Instructions.AdvanceNonceAccount.write(data);

return createInstruction(solanaAccounts.invokedSystemProgram(), keys, data);
}

public static Instruction withdrawNonceAccount(final SolanaAccounts solanaAccounts,
final PublicKey nonceAccount,
final PublicKey recipient,
final PublicKey nonceAuthority,
final long lamports) {
final var keys = List.of(
createWrite(nonceAccount),
createWrite(recipient),
solanaAccounts.readRecentBlockhashesSysVar(),
solanaAccounts.readRentSysVar(),
createReadOnlySigner(nonceAuthority)
);

final byte[] data = new byte[NATIVE_DISCRIMINATOR_LENGTH + Long.BYTES];
Instructions.WithdrawNonceAccount.write(data);
putInt64LE(data, 4, lamports);

return createInstruction(solanaAccounts.invokedSystemProgram(), keys, data);
}

public static Instruction initializeNonceAccount(final SolanaAccounts solanaAccounts,
final PublicKey nonceAccount,
final PublicKey nonceAuthority) {
final var keys = List.of(
createWrite(nonceAccount),
solanaAccounts.readRecentBlockhashesSysVar(),
solanaAccounts.readRentSysVar()
);

final byte[] data = new byte[NATIVE_DISCRIMINATOR_LENGTH + PUBLIC_KEY_LENGTH];
Instructions.InitializeNonceAccount.write(data);
nonceAuthority.write(data, NATIVE_DISCRIMINATOR_LENGTH);

return createInstruction(solanaAccounts.invokedSystemProgram(), keys, data);
}

public static Instruction authorizeNonceAccount(final AccountMeta invokedProgram,
final PublicKey nonceAccount,
final PublicKey currentNonceAuthority,
final PublicKey newNonceAuthority) {
final var keys = List.of(
createWrite(nonceAccount),
createReadOnlySigner(currentNonceAuthority)
);

final byte[] data = new byte[NATIVE_DISCRIMINATOR_LENGTH + PUBLIC_KEY_LENGTH];
Instructions.AuthorizeNonceAccount.write(data);
newNonceAuthority.write(data, NATIVE_DISCRIMINATOR_LENGTH);

return createInstruction(invokedProgram, keys, data);
}

public static Instruction authorizeNonceAccount(final SolanaAccounts solanaAccounts,
final PublicKey nonceAccount,
final PublicKey currentNonceAuthority,
final PublicKey newNonceAuthority) {
return authorizeNonceAccount(
solanaAccounts.invokedSystemProgram(),
nonceAccount,
currentNonceAuthority,
newNonceAuthority
);
}

public static Instruction upgradeNonceAccount(final AccountMeta invokedProgram, final PublicKey nonceAccount) {
final var keys = List.of(createWrite(nonceAccount));

final byte[] data = new byte[NATIVE_DISCRIMINATOR_LENGTH];
Instructions.UpgradeNonceAccount.write(data);

return createInstruction(invokedProgram, keys, data);
}

public static Instruction upgradeNonceAccount(final SolanaAccounts solanaAccounts, final PublicKey nonceAccount) {
return upgradeNonceAccount(solanaAccounts.invokedSystemProgram(), nonceAccount);
}

private SystemProgram() {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import software.sava.core.accounts.SolanaAccounts;
import software.sava.core.encoding.Base58;

import java.util.Base64;

import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static software.sava.core.accounts.SolanaAccounts.MAIN_NET;
Expand Down Expand Up @@ -42,6 +44,24 @@ public void createAccountInstruction() {
);

assertEquals("11119os1e9qSs2u7TsThXqkBSRUo9x7kpbdqtNNbTeaxHGPdWbvoHsks9hpp6mb2ed1NeB",
Base58.encode(instruction.data()));
Base58.encode(instruction.data())
);
}


@Test
public void parseNonceAccount() {
final var base64Data = "AQAAAAEAAAAM9WXp4HSq1hKViJ/hvS0dbhl8yvNJy13z3Lc8uGCyBirl7d+e05ILHtmpCyrZqMRG/x5AzISLYbViohfeG07tiBMAAAAAAAA=";
final byte[] data = Base64.getDecoder().decode(base64Data);
final var nonceAccount = NonceAccount.read(data, 0);

assertEquals(1, nonceAccount.version());
assertEquals(NonceAccount.State.Initialized, nonceAccount.state());
assertEquals(PublicKey.fromBase58Encoded("savaKKJmmwDsHHhxV6G293hrRM4f1p6jv6qUF441QD3"), nonceAccount.authority());

final byte[] blockHash = Base58.decode("3tTUV2sKPJ6zkS77Yo5D4vZnaqy3BX4WaTtmJsMwC2rQ");
assertArrayEquals(blockHash, nonceAccount.nonce());

assertEquals(5_000, nonceAccount.lamportsPerSignature());
}
}

0 comments on commit 4ec0d2b

Please sign in to comment.