diff --git a/jot-examples/src/main/java/com/method5/jot/examples/Config.java b/jot-examples/src/main/java/com/method5/jot/examples/Config.java index d3d930e..dc792a6 100644 --- a/jot-examples/src/main/java/com/method5/jot/examples/Config.java +++ b/jot-examples/src/main/java/com/method5/jot/examples/Config.java @@ -17,4 +17,8 @@ public class Config { "wss://asset-hub-westend-rpc.n.dwellir.com", "wss://asset-hub-westend.rpc.permanence.io") .toArray(String[]::new); + + public static final String[] FREQUENCY_WSS_SERVER = List.of( + "ws://localhost:9944" + ).toArray(String[]::new); } diff --git a/jot-examples/src/main/java/com/method5/jot/examples/extrinsic/BalancesTransferAllowDeathExample.java b/jot-examples/src/main/java/com/method5/jot/examples/extrinsic/BalancesTransferAllowDeathExample.java index 07ec134..eacba22 100644 --- a/jot-examples/src/main/java/com/method5/jot/examples/extrinsic/BalancesTransferAllowDeathExample.java +++ b/jot-examples/src/main/java/com/method5/jot/examples/extrinsic/BalancesTransferAllowDeathExample.java @@ -1,6 +1,9 @@ package com.method5.jot.examples.extrinsic; +import com.method5.jot.entity.DispatchError; +import com.method5.jot.events.EventRecord; import com.method5.jot.examples.Config; +import com.method5.jot.extrinsic.ExtrinsicResult; import com.method5.jot.extrinsic.call.Call; import com.method5.jot.query.model.AccountId; import com.method5.jot.rpc.PolkadotWs; @@ -11,6 +14,7 @@ import org.slf4j.LoggerFactory; import java.math.BigDecimal; +import java.util.List; public class BalancesTransferAllowDeathExample extends ExampleBase { private static final Logger logger = LoggerFactory.getLogger(BalancesTransferAllowDeathExample.class); @@ -18,7 +22,7 @@ public class BalancesTransferAllowDeathExample extends ExampleBase { public static void main(String[] args) throws Exception { Wallet wallet = Wallet.fromMnemonic(Config.MNEMONIC_PHRASE); - try (PolkadotWs api = new PolkadotWs(Config.WSS_SERVER, 10000)) { + try (PolkadotWs api = new PolkadotWs(Config.FREQUENCY_WSS_SERVER, 10000)) { execute(api, wallet.getSigner()); } } diff --git a/jot-examples/src/main/java/com/method5/jot/examples/extrinsic/BalancesTransferAllowDeathSignAndWaitForResultsExample.java b/jot-examples/src/main/java/com/method5/jot/examples/extrinsic/BalancesTransferAllowDeathSignAndWaitForResultsExample.java new file mode 100644 index 0000000..047d083 --- /dev/null +++ b/jot-examples/src/main/java/com/method5/jot/examples/extrinsic/BalancesTransferAllowDeathSignAndWaitForResultsExample.java @@ -0,0 +1,52 @@ +package com.method5.jot.examples.extrinsic; + +import com.method5.jot.events.EventRecord; +import com.method5.jot.examples.Config; +import com.method5.jot.extrinsic.ExtrinsicResult; +import com.method5.jot.extrinsic.call.Call; +import com.method5.jot.query.model.AccountId; +import com.method5.jot.rpc.PolkadotWs; +import com.method5.jot.signing.SigningProvider; +import com.method5.jot.wallet.Wallet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.math.BigDecimal; +import java.util.List; + +public class BalancesTransferAllowDeathSignAndWaitForResultsExample { + private static final Logger logger = LoggerFactory.getLogger(BalancesTransferAllowDeathSignAndWaitForResultsExample.class); + + public static void main(String[] args) throws Exception { + Wallet wallet = Wallet.fromMnemonic(Config.MNEMONIC_PHRASE); + + try (PolkadotWs api = new PolkadotWs(Config.FREQUENCY_WSS_SERVER, 10000)) { + execute(api, wallet.getSigner()); + } + } + + public static void execute(PolkadotWs api, SigningProvider signingProvider) throws Exception { + logger.info("Balances Transfer Allow Death (Using signAndWaitForResults) Example"); + logger.info("------------------------"); + + // Destination address + AccountId destination = AccountId.fromSS58("13NHcoGFJsHJoCYVsJrrv2ygLtz2XJSR17KrnA9QTNYz3Zkz"); + // Amount + BigDecimal amount = new BigDecimal("0.001"); + + Call call = api.tx().balances().transferAllowDeath(destination, amount); + + ExtrinsicResult result = call.signAndWaitForResults(signingProvider); + + List eventRecordList = result.getEvents(); + for (EventRecord eventRecord : eventRecordList) { + logger.info("Event: " + eventRecord.method()); + } + + String hash = result.getHash(); + + + logger.info("Extrinsic hash: {}", hash); + } + +} diff --git a/jot-examples/src/main/java/com/method5/jot/examples/extrinsic/CreateMsaExample.java b/jot-examples/src/main/java/com/method5/jot/examples/extrinsic/CreateMsaExample.java new file mode 100644 index 0000000..ade83b5 --- /dev/null +++ b/jot-examples/src/main/java/com/method5/jot/examples/extrinsic/CreateMsaExample.java @@ -0,0 +1,49 @@ +package com.method5.jot.examples.extrinsic; + +import com.method5.jot.events.EventRecord; +import com.method5.jot.examples.Config; +import com.method5.jot.extrinsic.ExtrinsicResult; +import com.method5.jot.extrinsic.call.Call; +import com.method5.jot.rpc.PolkadotWs; +import com.method5.jot.signing.SigningProvider; +import com.method5.jot.util.HexUtil; +import com.method5.jot.wallet.Wallet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.util.List; + +public class CreateMsaExample { + private static final Logger logger = LoggerFactory.getLogger(CreateMsaExample.class); + + public static void main(String[] args) throws Exception { + Wallet alice = Wallet.fromSr25519Seed(HexUtil.hexToBytes("0xe5be9a5092b81bca64be81d212e7f2f9eba183bb7a90954f7b76361f6edb5c0a")); + + try (PolkadotWs api = new PolkadotWs(Config.FREQUENCY_WSS_SERVER, 10000)) { + execute(api, alice.getSigner()); + } + } + + public static void execute(PolkadotWs api, SigningProvider signingProvider) throws Exception { + logger.info("Create MSA Example"); + logger.info("------------------------"); + + Call call = api.tx().msa().createMsa(); + + ExtrinsicResult result = call.signAndWaitForResults(signingProvider); + + List eventRecordList = result.getEvents(); + for (EventRecord eventRecord : eventRecordList) { + logger.info("Event: " + eventRecord.method()); + } + + String hash = result.getHash(); + + logger.info("Extrinsic hash: {}", hash); + + //Now try and get an error by sending the createMsa transaction again + + ExtrinsicResult failure = call.signAndWaitForResults(signingProvider); + + logger.info("Extrinsic dispatch error: " + failure.getError()); + } +} diff --git a/jot/pom.xml b/jot/pom.xml index 16560e3..4633b40 100644 --- a/jot/pom.xml +++ b/jot/pom.xml @@ -410,5 +410,11 @@ 5.20.0 test + + org.testcontainers + testcontainers-junit-jupiter + 2.0.1 + test + \ No newline at end of file diff --git a/jot/src/main/java/com/method5/jot/entity/DispatchError.java b/jot/src/main/java/com/method5/jot/entity/DispatchError.java index 6ecffd8..d276ae3 100644 --- a/jot/src/main/java/com/method5/jot/entity/DispatchError.java +++ b/jot/src/main/java/com/method5/jot/entity/DispatchError.java @@ -2,7 +2,10 @@ import com.method5.jot.metadata.CallIndexResolver; import com.method5.jot.metadata.RuntimeTypeDecoder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.util.ArrayList; import java.util.Map; import java.util.Objects; @@ -10,6 +13,7 @@ * DispatchError — class for dispatch error in the Jot SDK. Provides types and data models. */ public class DispatchError { + private static final Logger logger = LoggerFactory.getLogger(DispatchError.class); public enum Kind { MODULE, NAMED, @@ -21,6 +25,14 @@ public enum Kind { private final int errorCode; private final String name; + public String getName() { + return name; + } + + public int getModuleIndex() { + return moduleIndex; + } + private DispatchError(Kind kind, int moduleIndex, int errorCode, String name) { this.kind = kind; this.moduleIndex = moduleIndex; @@ -59,9 +71,22 @@ public static DispatchError decode(RuntimeTypeDecoder.TypeAndValue tv, CallIndex if ("Module".equals(variant)) { Map field0 = (Map) outer.get("field0"); + if (field0 != null) { - int index = (int) field0.getOrDefault("index", -1); - int error = (int) field0.getOrDefault("error", -1); + int index; + int error; + if(field0.getOrDefault("index", -1) instanceof Byte indexByte) { + index = ((Number) indexByte).intValue(); + } else { + index = (int) field0.getOrDefault("index", -1); + } + //Is there a better way to do this? + if(field0.get("error") instanceof ArrayList errorList) { + error = ((Number) errorList.getFirst()).intValue(); + } else { + error = (int) field0.getOrDefault("error", -1); + } + String name = resolver != null ? resolver.getModuleError(index, error) : "Unknown module error"; return module(index, error, name); } diff --git a/jot/src/main/java/com/method5/jot/extrinsic/call/AccountId.java b/jot/src/main/java/com/method5/jot/extrinsic/call/AccountId.java new file mode 100644 index 0000000..4159f82 --- /dev/null +++ b/jot/src/main/java/com/method5/jot/extrinsic/call/AccountId.java @@ -0,0 +1,28 @@ +package com.method5.jot.extrinsic.call; + +import java.util.Arrays; + +public class AccountId { + private final byte[] publicKeyBytes; + + public AccountId(byte[] publicKeyBytes) { + this.publicKeyBytes = publicKeyBytes; + } + + @Override + public boolean equals(Object other) { + if (this == other) return true; + if (!(other instanceof AccountId)) return false; + AccountId that = (AccountId) other; + return Arrays.equals(this.publicKeyBytes, that.publicKeyBytes); + } + + @Override + public int hashCode() { + return Arrays.hashCode(publicKeyBytes); + } + + public static AccountId fromBytes(byte[] data) { + return new AccountId(data); + } +} diff --git a/jot/src/main/java/com/method5/jot/extrinsic/call/EventClass.java b/jot/src/main/java/com/method5/jot/extrinsic/call/EventClass.java new file mode 100644 index 0000000..5334628 --- /dev/null +++ b/jot/src/main/java/com/method5/jot/extrinsic/call/EventClass.java @@ -0,0 +1,12 @@ +package com.method5.jot.extrinsic.call; + +import com.method5.jot.metadata.RuntimeTypeDecoder; + +import java.util.Map; + +public interface EventClass { + // Factory method that must create a new instance + static T create(Map attributes) { + throw new UnsupportedOperationException("Must be implemented by subclass"); + } +} \ No newline at end of file diff --git a/jot/src/main/java/com/method5/jot/extrinsic/call/ExtrinsicError.java b/jot/src/main/java/com/method5/jot/extrinsic/call/ExtrinsicError.java new file mode 100644 index 0000000..5171646 --- /dev/null +++ b/jot/src/main/java/com/method5/jot/extrinsic/call/ExtrinsicError.java @@ -0,0 +1,4 @@ +package com.method5.jot.extrinsic.call; + +public interface ExtrinsicError {} + diff --git a/jot/src/main/java/com/method5/jot/extrinsic/call/KeyAlreadyRegisteredError.java b/jot/src/main/java/com/method5/jot/extrinsic/call/KeyAlreadyRegisteredError.java new file mode 100644 index 0000000..b6b65e7 --- /dev/null +++ b/jot/src/main/java/com/method5/jot/extrinsic/call/KeyAlreadyRegisteredError.java @@ -0,0 +1,3 @@ +package com.method5.jot.extrinsic.call; + +public class KeyAlreadyRegisteredError implements ExtrinsicError{} diff --git a/jot/src/main/java/com/method5/jot/extrinsic/call/MessageSourceId.java b/jot/src/main/java/com/method5/jot/extrinsic/call/MessageSourceId.java new file mode 100644 index 0000000..d1069b5 --- /dev/null +++ b/jot/src/main/java/com/method5/jot/extrinsic/call/MessageSourceId.java @@ -0,0 +1,29 @@ +package com.method5.jot.extrinsic.call; + +import java.math.BigInteger; +import java.util.Objects; + +public class MessageSourceId { + private final BigInteger value; + + public MessageSourceId(BigInteger value) { + this.value = value; + } + + public BigInteger getValue() { + return value; + } + + @Override + public boolean equals(Object other) { + if (this == other) return true; + if (other == null || getClass() != other.getClass()) return false; + MessageSourceId that = (MessageSourceId) other; + return Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + return Objects.hash(value); + } +} diff --git a/jot/src/main/java/com/method5/jot/extrinsic/call/MsaPallet.java b/jot/src/main/java/com/method5/jot/extrinsic/call/MsaPallet.java new file mode 100644 index 0000000..55d9d36 --- /dev/null +++ b/jot/src/main/java/com/method5/jot/extrinsic/call/MsaPallet.java @@ -0,0 +1,60 @@ +package com.method5.jot.extrinsic.call; + +import com.method5.jot.metadata.RuntimeTypeDecoder; +import com.method5.jot.rpc.Api; +import com.method5.jot.rpc.CallOrQuery; +import com.method5.jot.scale.ScaleWriter; + +import java.math.BigInteger; +import java.util.*; + +public class MsaPallet extends CallOrQuery { + public MsaPallet(Api api) { + super(api); + } + + public Call createMsa() { + return new Call(api, createMsaWriter( + getResolver().resolveCallIndex("Msa", "create") + )); + } + + private byte[] createMsaWriter(byte[] callIndex) { + ScaleWriter writer = new ScaleWriter(); + writer.writeBytes(callIndex); + return writer.toByteArray(); + } + + public static class MsaCreated implements EventClass{ + private final MessageSourceId msaId; + private final AccountId accountId; + + public MsaCreated(MessageSourceId msaId, AccountId accountId) { + this.msaId = msaId; + this.accountId = accountId; + } + + public MessageSourceId getMsaId() { + return msaId; + } + + public AccountId getAccountId() { + return accountId; + } + + public static MsaCreated create(Map attributes) { + BigInteger msaIdValue = new BigInteger(attributes.get("MessageSourceId").getValue().toString()); + MessageSourceId msaId = new MessageSourceId(msaIdValue); + ArrayList byteList = (ArrayList) ((HashMap) attributes.get("T::AccountId").getValue()).get("field1"); + byte[] accountIdValue = new byte[byteList.size()]; + for (int i = 0; i < byteList.size(); i++) { + accountIdValue[i] = byteList.get(i); + } + AccountId accountId = new AccountId(accountIdValue); + + return new MsaCreated(msaId, accountId); + } + } + +} + diff --git a/jot/src/main/java/com/method5/jot/extrinsic/call/Transaction.java b/jot/src/main/java/com/method5/jot/extrinsic/call/Transaction.java index 1681992..bd279c9 100644 --- a/jot/src/main/java/com/method5/jot/extrinsic/call/Transaction.java +++ b/jot/src/main/java/com/method5/jot/extrinsic/call/Transaction.java @@ -10,6 +10,7 @@ public class Transaction { protected BalancesPallet balances; protected ConvictionVotingPallet convictionVoting; protected MultisigPallet multisig; + protected MsaPallet msa; protected StakingPallet staking; protected SystemPallet system; protected UtilityPallet utility; @@ -20,6 +21,7 @@ public Transaction(Api api) { balances = new BalancesPallet(api); convictionVoting = new ConvictionVotingPallet(api); multisig = new MultisigPallet(api); + msa = new MsaPallet(api); staking = new StakingPallet(api); system = new SystemPallet(api); utility = new UtilityPallet(api); @@ -37,6 +39,10 @@ public MultisigPallet multisig() { return multisig; } + public MsaPallet msa() { + return msa; + } + public StakingPallet staking() { return staking; } diff --git a/jot/src/main/java/com/method5/jot/metadata/CallIndexResolver.java b/jot/src/main/java/com/method5/jot/metadata/CallIndexResolver.java index f2e18f8..520a56a 100644 --- a/jot/src/main/java/com/method5/jot/metadata/CallIndexResolver.java +++ b/jot/src/main/java/com/method5/jot/metadata/CallIndexResolver.java @@ -18,7 +18,7 @@ public class CallIndexResolver { private final Map callMap = new HashMap<>(); private final Map moduleIndexToName = new HashMap<>(); private final Map> moduleFunctions = new HashMap<>(); - private final Map> moduleEvents = new HashMap<>(); + public final Map> moduleEvents = new HashMap<>(); private final Map> moduleErrors = new HashMap<>(); public CallIndexResolver() {} diff --git a/jot/src/test/java/com/method5/jot/extrinsic/E2ETest.java b/jot/src/test/java/com/method5/jot/extrinsic/E2ETest.java new file mode 100644 index 0000000..d6e3461 --- /dev/null +++ b/jot/src/test/java/com/method5/jot/extrinsic/E2ETest.java @@ -0,0 +1,113 @@ +package com.method5.jot.extrinsic; + +import com.method5.jot.events.EventRecord; +import com.method5.jot.extrinsic.call.*; +import com.method5.jot.rpc.PolkadotWs; +import com.method5.jot.signing.SigningProvider; +import com.method5.jot.util.HexUtil; +import com.method5.jot.wallet.Wallet; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import java.util.List; +import java.util.Map; +import java.util.Objects; + +@Testcontainers +public class E2ETest { + public final static String FREQUENCY_VERSION = "v1.17.5"; + + @Container + public static GenericContainer frequencyTestContainer = new GenericContainer<>(DockerImageName.parse(String.format("frequencychain/standalone-node:%s", FREQUENCY_VERSION))) + .withExposedPorts(30333, 9944, 9933) + .waitingFor(org.testcontainers.containers.wait.strategy.Wait.forLogMessage(".*Running JSON-RPC server.*", 1)); + + @BeforeAll + public static void setup() { + frequencyTestContainer.start(); + } + + public static String getWsAddress() { + return String.format("ws://%s:%s", frequencyTestContainer.getHost(), frequencyTestContainer.getMappedPort(9944)); + } + + public Class> mapEventNameToEventClass(String methodName){ + switch (methodName) { + case "MsaCreated": + return MsaPallet.MsaCreated.class; + default: + return null; + } + } + + public Class mapErrorNameToErrorClass(String errorName, int moduleIndex){ + switch (errorName) { + case "KeyAlreadyRegistered": + return KeyAlreadyRegisteredError.class; + default: + return null; + } + } + + @Test + public void msaTest() { + try(PolkadotWs api = new PolkadotWs(getWsAddress())) { + String chain = api.query().system().chain(); + Assertions.assertEquals("Frequency Development (No Relay)", chain); + + Wallet alice = Wallet.fromSr25519Seed(HexUtil.hexToBytes("0xe5be9a5092b81bca64be81d212e7f2f9eba183bb7a90954f7b76361f6edb5c0a")); + Assertions.assertEquals("5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY", alice.getAddress(42)); + + SigningProvider aliceSigningProvider = alice.getSigner(); + + Call call = api.tx().msa().createMsa(); + + ExtrinsicResult result = call.signAndWaitForResults(aliceSigningProvider); + + List eventRecordList = result.getEvents(); + for (EventRecord eventRecord : eventRecordList) { + System.out.println("Event: " + eventRecord.method()); + } + + EventRecord msaCreated = eventRecordList.stream().filter(r -> Objects.equals(r.method(), "MsaCreated")).toList().getFirst(); + Assertions.assertNotNull(msaCreated); + + Class> eventClass = mapEventNameToEventClass(msaCreated.method()); + + Object msaCreatedObject = eventClass.getMethod("create", Map.class).invoke(null, msaCreated.attributes()); + + Assertions.assertInstanceOf(MsaPallet.MsaCreated.class, msaCreatedObject); + Assertions.assertEquals(1, ((MsaPallet.MsaCreated) msaCreatedObject).getMsaId().getValue().intValue()); + + String hash = result.getHash(); + + System.out.println("Extrinsic hash: " + hash); + + ExtrinsicResult failure = call.signAndWaitForResults(aliceSigningProvider); + + //Step 1 dereference Error name to Error object type (moduleIndex, name) should be unique enough + //Step 2 There are no error attributes so you can just use an enum from step 1 + + + System.out.println(failure.getError().toHuman()); + Assertions.assertEquals("Module[60] Error[0]: KeyAlreadyRegistered", failure.getError().toHuman()); + + Object error = mapErrorNameToErrorClass(failure.getError().getName(), failure.getError().getModuleIndex()); + Assertions.assertInstanceOf(KeyAlreadyRegisteredError.class, error); + + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @AfterAll + public static void teardown() { + frequencyTestContainer.stop(); + } +} diff --git a/jot/src/test/java/com/method5/jot/extrinsic/MsaPalletTest.java b/jot/src/test/java/com/method5/jot/extrinsic/MsaPalletTest.java new file mode 100644 index 0000000..7e6d50a --- /dev/null +++ b/jot/src/test/java/com/method5/jot/extrinsic/MsaPalletTest.java @@ -0,0 +1,21 @@ +package com.method5.jot.extrinsic; + +import com.method5.jot.TestBase; +import com.method5.jot.util.HexUtil; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class MsaPalletTest extends TestBase { + + @Disabled("Metadata has to be updated to use a frequency one, but that'll screw up all the other tests") + @Test + public void testCreateMsa() { + byte[] callData = api.tx().msa().createMsa().callData(); + assertNotNull(callData); + assertEquals("3c00", HexUtil.bytesToHex(callData)); + } +} diff --git a/jot/src/test/java/com/method5/jot/metadata/MetadataParserTest.java b/jot/src/test/java/com/method5/jot/metadata/MetadataParserTest.java index 5ebc7e3..4706d76 100644 --- a/jot/src/test/java/com/method5/jot/metadata/MetadataParserTest.java +++ b/jot/src/test/java/com/method5/jot/metadata/MetadataParserTest.java @@ -22,16 +22,20 @@ public void testParseMetadata() { byte[] nonExistentCheck = resolver.resolveCallIndex("Null", "null"); byte[] transferIndex = resolver.resolveCallIndex("Balances", "force_transfer"); byte[] remarkIndex = resolver.resolveCallIndex("System", "remark"); +// byte[] createMsaIndex = resolver.resolveCallIndex("Msa", "create"); assertNull(nonExistentCheck); assertNotNull(transferIndex, "Balances.forceTransfer not registered"); assertNotNull(remarkIndex, "System.remark not registered"); +// assertNotNull(createMsaIndex, "Msa.remark not registered"); assertEquals(2, transferIndex.length, "Call index should be 2 bytes"); assertEquals(2, remarkIndex.length, "Call index should be 2 bytes"); +// assertEquals(2, createMsaIndex.length, "Call index should be 2 bytes"); logger.info("Balances.transfer = [{}, {}]", transferIndex[0], transferIndex[1]); logger.info("System.remark = [{}, {}]", remarkIndex[0], remarkIndex[1]); +// logger.info("Msa.create = [{}, {}]", createMsaIndex[0], createMsaIndex[1]); } @Test