diff --git a/README.md b/README.md index 45e8343..721e0cb 100644 --- a/README.md +++ b/README.md @@ -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 { @@ -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 + } } } @@ -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); +} +``` diff --git a/settings.gradle b/settings.gradle index 3e0c219..d17852c 100644 --- a/settings.gradle +++ b/settings.gradle @@ -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") } } } diff --git a/src/main/java/software/sava/solana/programs/system/NonceAccount.java b/src/main/java/software/sava/solana/programs/system/NonceAccount.java new file mode 100644 index 0000000..eb32c64 --- /dev/null +++ b/src/main/java/software/sava/solana/programs/system/NonceAccount.java @@ -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 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 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 + ); + } +} diff --git a/src/main/java/software/sava/solana/programs/system/SystemProgram.java b/src/main/java/software/sava/solana/programs/system/SystemProgram.java index 2ed82ab..bef9796 100644 --- a/src/main/java/software/sava/solana/programs/system/SystemProgram.java +++ b/src/main/java/software/sava/solana/programs/system/SystemProgram.java @@ -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; @@ -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() { } } diff --git a/src/test/java/software/sava/solana/programs/system/SystemProgramTest.java b/src/test/java/software/sava/solana/programs/system/SystemProgramTest.java index 7571826..02f261a 100644 --- a/src/test/java/software/sava/solana/programs/system/SystemProgramTest.java +++ b/src/test/java/software/sava/solana/programs/system/SystemProgramTest.java @@ -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; @@ -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()); } }