Solanoid is framework for testing and building programs on Solana blockchain written in Go.
Solanoid aims to fill the gap between writing and testing contracts on Solana. There's no yet built-in testing framework so we considered to present Solanoid.
- Go >= 1.15.
solana-cli
.
- Allows binding compiled smart contracts via symlinks for deployment via framework. Example: Details
...
bind-symlink "../solana-adapter/src/gravity-core-adapter" "nebula.so" "nebula/target/deploy/solana_nebula_contract.so"
## Example
## bind-symlink $project_root $binary_name $path_to_binary
...
- Provides examples for end-to-end testing. Example: full program test
- Provides abstractions for signing/sending transactions and calling programs. Example: caller abstraction, based on Nebula
- Provides helper functions for Solana. Example new data account
func GenerateNewTokenAccount(privateKey string, space uint64, owner, tokenMint common.PublicKey, clientEndpoint string, seeds string) (*models.CommandResponse, error)
- Provides instruction building approach. Example
type NebulaInstructionBuilder struct{}
var NebulaIXBuilder = &NebulaInstructionBuilder{}
func (port *NebulaInstructionBuilder) Init(bft, dataType uint8, gravityProgramID common.PublicKey, oracles []byte) interface{} {
return InitNebulaContractInstruction{
Instruction: 0,
Bft: bft,
NebulaDataType: dataType,
GravityContractProgramID: gravityProgramID,
InitialOracles: oracles,
}
}
-
Provides management of temporary and storage persistent private keys. Example Operational
-
Provides helper functions for interaction with
solana-cli
,spl-token
. Example solana.go -
Offers parallel deployment of programs. Example: Solana gateway deployment
ParallelExecution(
[]func(){
func() {
_, err = DeploySolanaProgram(t, "ibport", ibportProgram.PKPath, consulsList.List[0].PKPath, "../binaries/ibport.so")
ValidateError(t, err)
},
func() {
_, err = DeploySolanaProgram(t, "gravity", gravityProgram.PKPath, consulsList.List[1].PKPath, "../binaries/gravity.so")
ValidateError(t, err)
},
func() {
_, err = DeploySolanaProgram(t, "nebula", nebulaProgram.PKPath, consulsList.List[2].PKPath, "../binaries/nebula.so")
ValidateError(t, err)
},
},
)
- Deployment via tests. Example commands/gateway_test.go
- Facility for writing MVPs between Solana and EVM Solana and EVM Gateway MVP (Polygon is disabled atm)
To get the most of the Solanoid, follow these steps:
- Fork the repository. Clone it.
git clone <your_profile_or_org>/solanoid
-
If you build your own custom program: declare method signatures in
commands/executor
. -
Check this invocation without Multisig.
// commands/executor/helloworld.go
package executor
type SayHelloWorldIX struct {
Instruction uint8
Message string
}
type HelloWorldProgramIXBuilder struct{}
func (builder *HelloWorldProgramIXBuilder) SayHello(message string) interface{} {
return SayHelloWorldIX{
Instruction: 0,
Message: message,
}
}
// Then call your method via abstraction
// commands/helloworld_test.go
func TestHelloWorld(t *testing.T) {
deployer, err := NewOperatingAddress(t, "path_to_deployer", nil)
ValidateError(t, err)
helloWorldProgram, err := NewOperatingAddress(t, "path_to_program_address", nil)
ValidateError(t, err)
// RPC is inferred via `solana config get`
RPCEndpoint, _ := InferSystemDefinedRPC()
// deployment
_, err = DeploySolanaProgram(t, "helloworld", helloWorldProgram.PKPath, deployer.PKPath, "path_to_program_binary")
ValidateError(t, err)
// contract bytes allocation
HelloWorldContractAllocation := 750
helloWorldDataAccount, err := GenerateNewAccount(deployer.PrivateKey, HelloWorldContractAllocation, helloWorldProgram.PublicKey.ToBase58(), RPCEndpoint)
ValidateError(t, err)
helloWorldExecutor, err := InitGenericExecutor(
deployer.PrivateKey,
helloWorldProgram.PublicKey.ToBase58(),
helloWorldDataAccount.Account.PublicKey.ToBase58(),
"", // Empty if we want NO MULTISIG
RPCEndpoint,
common.PublicKeyFromString(""), // can be omitted always
)
ValidateError(t, err)
// we wait right before every method call
waitTransactionConfirmations()
ixbuilder := &executor.HelloWorldProgramIXBuilder{}
sayHelloResponse, err := helloWorldExecutor.BuildAndInvoke(
ixbuilder.SayHello("HELLO WORLD"),
)
fmt.Printf("Hello World Call Result: %v \n", sayHelloResponse.TxSignature)
ValidateError(t, err)
}
- For methods requiring multisig, just do this.
// inside test
...
// if we want additional accounts
// those are concated to the end of the list
// Like this:
// signer + <n> of multisigs + <n> of additional meta
currentExecutor.SetAdditionalMeta([]types.AccountMeta{
{PubKey: common.TokenProgramID, IsWritable: false, IsSigner: false},
})
multisigMember, err := ReadOperatingAddress(t, "path_to_multisig_member")
ValidateError(t, err)
var signers []executor.GravityBftSigner
signers = append(signers, *executor.NewGravityBftSigner(multisigMember.PrivateKey))
// For multisigs
currentExecutor.SetAdditionalSigners(signers)
// multisigs and metas can be erased
currentExecutor.EraseAdditionalMeta()
currentExecutor.EraseAdditionalSigners()
...
- Tests can be declared in
commands/
directory. - Custom data models - in
models/
.
Solanoid provides an example on how to write MVPs for dApps between EVM and Solana. Please consider check it here this gateway example between Solana and EVM.
- When writing tests consider awaiting till confirmations reach MAX (via
waitTransactionConfirmations()
call) - for Mainnet it's about 30 seconds, Devnet - 15 seconds. If you won't wait, state transition is not guaranteed. - Tests require temporary addresses to operate with. For such purpose use
NewOperatingAddress
function. - Deployment tests require persisten addresses. For such purpose use
ReadOperatingAddress
function.
- We have obvious responsibility on managing and keeping this repo up-to-date but we'll be happy to receive issues/PR from any contributor.
- There is a naming misconception in several functions due to a fact that Solanoid has been considered as a proprietary software for SuSy dApp. However, we think that this software should be opensource. We'll make Solanoid more generic.