How can I make a batch send transaction? - cosmos-sdk

I'm having trouble sending tokens to many addresses simultaneously. As far as I know, Cosmos does not support batch sending, so I have to make one tx for each recipient, and make sure the account sequence (nonce) is correct for each tx.
So if I specify the account sequence manually, I can create many send transactions at once - but I still have to wait for the node to confirm one tx and update the account sequence before sending the next one.
What can one do in this case?

Cosmos actually does support a MultiSend operation on the bank module, unfortunately it is not wired into the clients for support typically however it is widely used by exhanges such as Coinbase to optimize transfers.
https://github.com/cosmos/cosmos-sdk/blob/e957fad1a7ffd73712cd681116c9b6e09fa3e60b/x/bank/keeper/msg_server.go#L73

I have since been reminded that a single Cosmos transaction can consist of multiple Messages, where each Message is a message to the bank module to send tokens to one account. That's how batch sending works in Cosmos.
Unfortunately this is not available via the command line, so one has to use the SDK to make such a transaction.

Here's a link to CosmJS demonstration of sending a transaction with two messages and two signatures on the v0.39.x LTS release of the Cosmos SDK here (hat tip Ethan Frey from Confio for pointing this out)
In case the link changes at any point here's the code:
describe("appendSignature", () => {
it("works", async () => {
pendingWithoutLaunchpad();
const wallet0 = await Secp256k1HdWallet.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(0));
const wallet1 = await Secp256k1HdWallet.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(1));
const client0 = new SigningCosmosClient(launchpad.endpoint, faucet.address0, wallet0);
const client1 = new SigningCosmosClient(launchpad.endpoint, faucet.address1, wallet1);
const msg1: MsgSend = {
type: "cosmos-sdk/MsgSend",
value: {
from_address: faucet.address0,
to_address: makeRandomAddress(),
amount: coins(1234567, "ucosm"),
},
};
const msg2: MsgSend = {
type: "cosmos-sdk/MsgSend",
value: {
from_address: faucet.address1,
to_address: makeRandomAddress(),
amount: coins(1234567, "ucosm"),
},
};
const fee = {
amount: coins(2000, "ucosm"),
gas: "160000", // 2*80k
};
const memo = "This must be authorized by the two of us";
const signed = await client0.sign([msg1, msg2], fee, memo);
const cosigned = await client1.appendSignature(signed);
expect(cosigned.msg).toEqual([msg1, msg2]);
expect(cosigned.fee).toEqual(fee);
expect(cosigned.memo).toEqual(memo);
expect(cosigned.signatures).toEqual([
{
pub_key: faucet.pubkey0,
signature: jasmine.stringMatching(base64Matcher),
},
{
pub_key: faucet.pubkey1,
signature: jasmine.stringMatching(base64Matcher),
},
]);
// Ensure signed transaction is valid
const broadcastResult = await client0.broadcastTx(cosigned);
assertIsBroadcastTxSuccess(broadcastResult);
});
});
For doing this kind of thing in Golang, like with the Cosmos SDK client CLI, you can take the distribution module as an example: https://github.com/cosmos/cosmos-sdk/blob/master/x/distribution/client/cli/tx.go#L165-L180
msgs := make([]sdk.Msg, 0, len(validators))
for _, valAddr := range validators {
val, err := sdk.ValAddressFromBech32(valAddr)
if err != nil {
return err
}
msg := types.NewMsgWithdrawDelegatorReward(delAddr, val)
if err := msg.ValidateBasic(); err != nil {
return err
}
msgs = append(msgs, msg)
}
chunkSize, _ := cmd.Flags().GetInt(FlagMaxMessagesPerTx)
return newSplitAndApply(tx.GenerateOrBroadcastTxCLI, clientCtx, cmd.Flags(), msgs, chunkSize)

Related

MassTransit StateMachine - How to publish messages, where the message type can be identified only at runtime

I have StateMachine, After each EventCompletion and State Changes. It publishes one or more Commands using StateMachine.Context.Publish like below.
if (req.GetType() == typeof(ValuationRun_WfReq))
{
var obj = PreparePayLoad<ValuationRun_WfReq>(wfTask, req, context.Instance.CorrelationId);
obj.CorrelationId = context.Instance.CorrelationId;
await context.Publish(obj);
}
else if (req.GetType() == typeof(MonthEndStep2_WfReq))
{
var obj = PreparePayLoad<MonthEndStep2_WfReq>(wfTask, req, context.Instance.CorrelationId);
obj.CorrelationId = context.Instance.CorrelationId;
await context.Publish(obj);
}
else if (req.GetType() == typeof(MonthEndStep5_WfReq))
{
var obj = PreparePayLoad<MonthEndStep5_WfReq>(wfTask, req, context.Instance.CorrelationId);
obj.CorrelationId = context.Instance.CorrelationId;
await context.Publish(obj);
}
As each published message should go to its own queue. I have to expicitly add the If Conditions to check the Type and Publish with that Type.
This way I have more than 20 Type and adding If Conditions for each message type.
This I have to change to identify required Message Type from AppSettings and publish the right queue.

Cant use jwt connect nats by my code,its doesnt work

I want to use Nats websocket but I don't know how to get user information so I use jwt and it works but it doesn't work when I use my code to generate user jwt.
nats server cfg
websocket
{
port: 8080
no_tls: true
# authorization {
# # If this is specified, the client has to provide the same username
# # and password to be able to connect.
# # username: "my_user_name"
# # password: "my_password"
#
# # If this is specified, the password field in the CONNECT has to
# # match this token.
# # token: "my_token"
#
# # This overrides the main's authorization timeout. For consistency
# # with the main's authorization configuration block, this is expressed
# # as a number of seconds.
# # timeout: 2.0
#}
}
# Operator named my_org
operator: eyJ0eXAiOiJKV1QiLCJhbGciOiJlZDI1NTE5LW5rZXkifQ.eyJqdGkiOiI3VVkzSkNaVUlVQUU0STZJWllBQ0NZUEdDWDdQQUlHRzczQklNTzVBT1NZMjJXQ1JZWDdRIiwiaWF0IjoxNjY4NDE3NDkzLCJpc3MiOiJPQzRLV0xGNU5CQkhPVjVLRFJGS0NITFkySEpBRFNYQUg2VEJHSk1XSEFVSFg3VlVOTkNTRVdJUyIsIm5hbWUiOiJteV9vcmciLCJzdWIiOiJPQzRLV0xGNU5CQkhPVjVLRFJGS0NITFkySEpBRFNYQUg2VEJHSk1XSEFVSFg3VlVOTkNTRVdJUyIsIm5hdHMiOnsic2lnbmluZ19rZXlzIjpbIk9DUFdTV0NQSlJNMlRDUjVIVjZFNkNFVklRV09MNExLNEJPS1VQRk5DNzNJVktZM0xIN0VVRTZZIl0sImFjY291bnRfc2VydmVyX3VybCI6Im5hdHM6Ly8wLjAuMC4wOjQyMjIiLCJzeXN0ZW1fYWNjb3VudCI6IkFDU0dDWENUVFpLWlVCRkFIN1lFR01HTkhQRFRPQlRJRUdONFlHS1JWT1hXT1FOM1Y2T1NVS1Q1Iiwic3RyaWN0X3NpZ25pbmdfa2V5X3VzYWdlIjp0cnVlLCJ0eXBlIjoib3BlcmF0b3IiLCJ2ZXJzaW9uIjoyfX0.axLP53rM3O2R6XNMagyX4vnBoYCp7DCA2lptVlX2i4lLdbN9x5Vm4eYP-7yG7kMqDG9rPG6HmgCyYoQndqpuAw
# System Account named SYS
system_account: ACSGCXCTTZKZUBFAH7YEGMGNHPDTOBTIEGN4YGKRVOXWOQN3V6OSUKT5
# configuration of the nats based resolver
resolver {
type: full
# Directory in which the account jwt will be stored
dir: './jwt'
# In order to support jwt deletion, set to true
# If the resolver type is full delete will rename the jwt.
# This is to allow manual restoration in case of inadvertent deletion.
# To restore a jwt, remove the added suffix .delete and restart or send a reload signal.
# To free up storage you must manually delete files with the suffix .delete.
allow_delete: false
# Interval at which a nats-server with a nats based account resolver will compare
# it's state with one random nats based account resolver in the cluster and if needed,
# exchange jwt and converge on the same set of jwt.
interval: "2m"
# Timeout for lookup requests in case an account does not exist locally.
timeout: "1.9s"
}
# Preload the nats based resolver with the system account jwt.
# This is not necessary but avoids a bootstrapping system account.
# This only applies to the system account. Therefore other account jwt are not included here.
# To populate the resolver:
# 1) make sure that your operator has the account server URL pointing at your nats servers.
# The url must start with: "nats://"
# nsc edit operator --account-jwt-server-url nats://localhost:4222
# 2) push your accounts using: nsc push --all
# The argument to push -u is optional if your account server url is set as described.
# 3) to prune accounts use: nsc push --prune
# In order to enable prune you must set above allow_delete to true
# Later changes to the system account take precedence over the system account jwt listed here.
resolver_preload: {
ACSGCXCTTZKZUBFAH7YEGMGNHPDTOBTIEGN4YGKRVOXWOQN3V6OSUKT5: eyJ0eXAiOiJKV1QiLCJhbGciOiJlZDI1NTE5LW5rZXkifQ.eyJqdGkiOiJMWUI0S0tCN0dKQTYyQTZMVk1BS1hFRlRWUE1DUkRQQVhBNktBSEZNTlZWWkIzSktCVlJRIiwiaWF0IjoxNjY4NDE3NDY5LCJpc3MiOiJPQ1BXU1dDUEpSTTJUQ1I1SFY2RTZDRVZJUVdPTDRMSzRCT0tVUEZOQzczSVZLWTNMSDdFVUU2WSIsIm5hbWUiOiJTWVMiLCJzdWIiOiJBQ1NHQ1hDVFRaS1pVQkZBSDdZRUdNR05IUERUT0JUSUVHTjRZR0tSVk9YV09RTjNWNk9TVUtUNSIsIm5hdHMiOnsiZXhwb3J0cyI6W3sibmFtZSI6ImFjY291bnQtbW9uaXRvcmluZy1zdHJlYW1zIiwic3ViamVjdCI6IiRTWVMuQUNDT1VOVC4qLlx1MDAzZSIsInR5cGUiOiJzdHJlYW0iLCJhY2NvdW50X3Rva2VuX3Bvc2l0aW9uIjozLCJkZXNjcmlwdGlvbiI6IkFjY291bnQgc3BlY2lmaWMgbW9uaXRvcmluZyBzdHJlYW0iLCJpbmZvX3VybCI6Imh0dHBzOi8vZG9jcy5uYXRzLmlvL25hdHMtc2VydmVyL2NvbmZpZ3VyYXRpb24vc3lzX2FjY291bnRzIn0seyJuYW1lIjoiYWNjb3VudC1tb25pdG9yaW5nLXNlcnZpY2VzIiwic3ViamVjdCI6IiRTWVMuUkVRLkFDQ09VTlQuKi4qIiwidHlwZSI6InNlcnZpY2UiLCJyZXNwb25zZV90eXBlIjoiU3RyZWFtIiwiYWNjb3VudF90b2tlbl9wb3NpdGlvbiI6NCwiZGVzY3JpcHRpb24iOiJSZXF1ZXN0IGFjY291bnQgc3BlY2lmaWMgbW9uaXRvcmluZyBzZXJ2aWNlcyBmb3I6IFNVQlNaLCBDT05OWiwgTEVBRlosIEpTWiBhbmQgSU5GTyIsImluZm9fdXJsIjoiaHR0cHM6Ly9kb2NzLm5hdHMuaW8vbmF0cy1zZXJ2ZXIvY29uZmlndXJhdGlvbi9zeXNfYWNjb3VudHMifV0sImxpbWl0cyI6eyJzdWJzIjotMSwiZGF0YSI6LTEsInBheWxvYWQiOi0xLCJpbXBvcnRzIjotMSwiZXhwb3J0cyI6LTEsIndpbGRjYXJkcyI6dHJ1ZSwiY29ubiI6LTEsImxlYWYiOi0xfSwic2lnbmluZ19rZXlzIjpbIkFEU05HUk5WRUhIWFU0SFdUNk80NTQyVFVLSlVER0ZCNU9DTzZYQTNHVE9NTklBMjMyUU9LQzRFIl0sImRlZmF1bHRfcGVybWlzc2lvbnMiOnsicHViIjp7fSwic3ViIjp7fX0sInR5cGUiOiJhY2NvdW50IiwidmVyc2lvbiI6Mn19.DTH_ubEJpwPIj2tmr1eg8nI_HgKvFFqhQ0iL17fT8iy1bJ1AR_jnXg7CKNakYQrdb4pjEBzzpMoH_mbguSdGAQ,
}
When i use nsc client tools its work,
nsc add user --account TEAM_B math
nsc generate creds -n math > math.creds
When i use my code it doesnt work
package main
import (
"github.com/nats-io/jwt/v2"
"github.com/nats-io/nats.go"
"github.com/nats-io/nkeys"
"time"
)
func main() {
ukp, err := nkeys.CreateUser()
if err != nil {
return
}
upub, err := ukp.PublicKey()
if err != nil {
return
}
seed, err := ukp.Seed()
if err != nil {
return
}
akp, _ := nkeys.FromSeed([]byte("SAAFREANAV7DLYTGDCST76AHUOAMK7CTK5RNJWPERHWEFPR7NXEHRTHUWI"))
userJWT := generateUserJWT(upub, akp)
jwtAuthOption := nats.UserJWTAndSeed(userJWT, string(seed))
nc, err := nats.Connect("nats://localhost:4222", jwtAuthOption)
if err != nil {
panic(err)
}
defer nc.Close()
}
func generateUserJWT(userPublicKey string, accountSigningKey nkeys.KeyPair) (userJWT string) {
uc := jwt.NewUserClaims(userPublicKey)
uc.Expires = time.Now().Add(time.Hour).Unix() // expire in an hour
var err error
uc.IssuerAccount, err = accountSigningKey.PublicKey()
if err != nil {
return ""
}
vr := jwt.ValidationResults{}
uc.Validate(&vr)
if vr.IsBlocking(true) {
panic("Generated user claim is invalid")
}
userJWT, err = uc.Encode(accountSigningKey)
if err != nil {
return ""
}
return
}
here is refence
https://docs.nats.io/running-a-nats-service/nats_admin/security/jwt#create-user-jwt
https://github.com/ConnectEverything/rethink_connectivity_examples/tree/main/episode7
my code generate jwt
its panic Authorization Violation
here is my FE code
import './App.css';
import {connect, jwtAuthenticator,credsAuthenticator, StringCodec} from 'nats.ws'
import {useEffect, useState} from "react";
const sc = StringCodec()
function App() {
const c=`-----BEGIN NATS USER JWT-----
eyJ0eXAiOiJKV1QiLCJhbGciOiJlZDI1NTE5LW5rZXkifQ.eyJqdGkiOiJWNFRMQU9aQjY2M1NJT0JCV1RCVkpWVVJOQkdLTUJBUERGVVk2WVUzM1JHQTRXRFBSSkdBIiwiaWF0IjoxNjY4NDkxODE4LCJpc3MiOiJBQTVZTzRRQVVQREhEWVFQR1lKMlRQN1dEV1RGVkNHUlVXVzdFSTVLQUhHM1RRWlVHMkdRVTZYSyIsIm5hbWUiOiJteS11c2VyIiwic3ViIjoiVURXRkFQVVUzWlAyQ1VFVkRWWkpBRjJRM0hLNlJLSU5YS0xQWjNYV0VFWkxJRE00RjdWMzZXM0MiLCJuYXRzIjp7InB1YiI6eyJhbGxvdyI6WyJmb28uXHUwMDNlIiwiYmFyLlx1MDAzZSJdfSwic3ViIjp7ImFsbG93IjpbIl9JTkJPWC5cdTAwM2UiXX0sInN1YnMiOi0xLCJkYXRhIjoxMDczNzQxODI0LCJwYXlsb2FkIjotMSwidHlwZSI6InVzZXIiLCJ2ZXJzaW9uIjoyfX0.6Zg8ekHENudDY2gT5hVfXomnQ1tGfHT7O__FrewjWXH3oaWPy81Qr7_U1ZzmuWPirTq4JsZjoOnV9TxmrwywCA
------END NATS USER JWT------
************************* IMPORTANT *************************
NKEY Seed printed below can be used to sign and prove identity.
NKEYs are sensitive and should be treated as secrets.
-----BEGIN USER NKEY SEED-----
SUAIRSUPV65OG3S5C66DIMLNY2IXNUSHT6QEBFMWXGBM7G3EGPJO3XHELE
------END USER NKEY SEED------
*************************************************************
`
const [nc, setConnection] = useState(undefined)
const [lastError, setError] = useState("")
const [messages, setMessages] = useState([])
let key = 0
const me = {id:"dddd",name:"ff"};
const addMessage = (err, msg) => {
if (err){
console.log(err)
}
key++;
const {subject, reply} = msg;
const data = sc.decode(msg.data)
console.log("msg==",subject,' data=',data)
const m = {subject, reply, data, key, time: new Date().toUTCString()}
messages.unshift(m)
const a = messages.slice(0, 10)
messages.unshift(a)
setMessages(a)
}
const who = (err,msg)=>{
msg.respond(me)
const {subject, reply} = msg;
const data = sc.decode(msg.data)
console.log("who==",subject,' data=',data)
}
const entered = (err,msg)=>{
const {subject, reply} = msg;
const data = sc.decode(msg.data)
console.log("en==",subject,' data=',data)
}
const exited = (err,msg)=>{
const {subject, reply} = msg;
const data = sc.decode(msg.data)
console.log("exit==",subject,' data=',data)
}
useEffect(() => {
console.log(nc)
if (nc === undefined) {
//connect({servers: ["nats://127.0.0.1:4222"],
connect({servers: ["ws://127.0.0.1:8083"],
//work //authenticator:jwtAuthenticator("eyJ0eXAiOiJKV1QiLCJhbGciOiJlZDI1NTE5LW5rZXkifQ.eyJqdGkiOiJCQ1kyRjUyRUxIU1kzV0RYUkhUU0FLUjU2TEZCWU4yUjdNSU5CWENNUUVZQ0JJNUVXQzNBIiwiaWF0IjoxNjY4NDE3NTI2LCJpc3MiOiJBQkRVWE9WTkdLV1o1QUlGR1g1VUdRSFJCUVo3S0dIWk9URDdOVTRWTVBKU1BRTkdXNkJNQjJQTCIsIm5hbWUiOiJtYXRoIiwic3ViIjoiVUE0RTNSRVU1Nlo3MjM2U0hTUURQSUY1UlhWRzJXVlkzV0NPRlJGRENSQkRTWkNWQkJPRFJQVkgiLCJuYXRzIjp7InB1YiI6e30sInN1YiI6e30sInN1YnMiOi0xLCJkYXRhIjotMSwicGF5bG9hZCI6LTEsImlzc3Vlcl9hY2NvdW50IjoiQUNYRElYRElJSVVUT1YzWDZITVVWSEUyTEJNUDI0N1FHS0IyWkFCQTdWSElGWjdKRU1TVUxTSkwiLCJ0eXBlIjoidXNlciIsInZlcnNpb24iOjJ9fQ.tDfuREQDIFiIOlAD1fe7jkrVPiaRSoAwcRa_e4G3AVby97XSssEN_EQCeT60WomOo1fHIFV9hgMCuPHQAaL_Ag", new TextEncoder().encode("SUALJXSMUDYDDKWMWLREDHEEBA7HZA5FJVJFVUYMELSQNT2BJJ3J665RDQ")),
//work //authenticator:jwtAuthenticator("eyJ0eXAiOiJKV1QiLCJhbGciOiJlZDI1NTE5LW5rZXkifQ.eyJqdGkiOiJUMlI3RURSVVNESUxJUUNKVFRGUVhLNVJaRUsyNlFPWURFNFozQzdQREFKUVRJWkhQUllRIiwiaWF0IjoxNjY4NDgyOTA5LCJpc3MiOiJBQkRVWE9WTkdLV1o1QUlGR1g1VUdRSFJCUVo3S0dIWk9URDdOVTRWTVBKU1BRTkdXNkJNQjJQTCIsIm5hbWUiOiJtYXRoMTEiLCJzdWIiOiJVQUw2SFdGQlhBVUFBUE1LR1RYM1JSVVkySFc3QVZXRVBDUFBIUFlMWU9VM1oyVTVPQzZLSDdFWiIsIm5hdHMiOnsicHViIjp7fSwic3ViIjp7fSwic3VicyI6LTEsImRhdGEiOi0xLCJwYXlsb2FkIjotMSwiaXNzdWVyX2FjY291bnQiOiJBQ1hESVhESUlJVVRPVjNYNkhNVVZIRTJMQk1QMjQ3UUdLQjJaQUJBN1ZISUZaN0pFTVNVTFNKTCIsInR5cGUiOiJ1c2VyIiwidmVyc2lvbiI6Mn19.7HovnuwbJvQCjiofMLjlT_ASa2k2xA8_biCOx-KWbGcj11kptVSsFZHKqm6ppg3OM8klNvCwDNJhJHhx0U8uAQ", new TextEncoder().encode("SUACFBD4BOQ4AWR2BG5SPLOJXFIFSWPCSY3ZS25YN5KLQXD5QKBJNMUNQQ")),
// my jwt doesn't work
authenticator:jwtAuthenticator("eyJ0eXAiOiJKV1QiLCJhbGciOiJlZDI1NTE5LW5rZXkifQ.eyJleHAiOjE2Njg1MDg2OTgsImp0aSI6IkZVMkhPRjJLMkNaWU42UEtZNTNWQUQyUlpXVjdXSEJMTzJEVVhHUzVZVEFSS1BYQjcyWkEiLCJpYXQiOjE2Njg1MDUwOTksImlzcyI6IkFDU0dDWENUVFpLWlVCRkFIN1lFR01HTkhQRFRPQlRJRUdONFlHS1JWT1hXT1FOM1Y2T1NVS1Q1Iiwic3ViIjoiVUFTV0NVSzVCWVpSTVRVU0ZBVjNFT1pFUzRSMzNWRkc1REZKVUtKM1ZLSUtQTDZCNUEyNFI1UkciLCJuYXRzIjp7InB1YiI6e30sInN1YiI6e30sInN1YnMiOi0xLCJkYXRhIjotMSwicGF5bG9hZCI6LTEsImlzc3Vlcl9hY2NvdW50IjoiQUNTR0NYQ1RUWktaVUJGQUg3WUVHTUdOSFBEVE9CVElFR040WUdLUlZPWFdPUU4zVjZPU1VLVDUiLCJ0eXBlIjoidXNlciIsInZlcnNpb24iOjJ9fQ.6-pWmq1QUkmDGH6pZuXSm6pgE_VgHoJjukCMIQN6p3j5vFV5YrRWO48IDzKobUm1De4wkZHgGJZiFctM2PpDAA", new TextEncoder().encode("SUAFPCMKO6X6K2Z4GHKK7OXCLI3Q7VWOCJBIROLTX6ILSW2W7HZBJEDBJ4")),
//authenticator:credsAuthenticator(new TextEncoder().encode(c)),
waitOnFirstConnect: true,noEcho:true,
}).then(
(nc) => {
setConnection(nc)
nc.subscribe('>', {callback: addMessage})
nc.subscribe('user.who',{callback:who})
nc.subscribe('user.*.entered',{callback:entered})
nc.subscribe('user.*.exit',{callback:exited})
nc.publish('user.bob#bob.com.entered',sc.encode(me))
}
).catch((err) => {
setError(err)
console.log(lastError)
})
}
})
const state = nc ? 'connected' : "not yet con"
return (
<div className="container">
<h1>{state}</h1>
</div>
);
}
export default App;
Just to make sure: did you use nsc push your changes after adding user math? You configured your nats server with a resolver configuration. This is good, but your resolver needs the account information to verify your user.

Integrate actix websocket with rabbitmq (lapin) in Rust

I've written a websocket server in Rust using actix. If anyone wants to check the full repo here https://github.com/fabracht/actix-websocket.
Now, I want to integrate rabbitmq into the project. For that, I found the lapin crate https://docs.rs/lapin/1.8.0/lapin/. But I'm having problems integrating it with the actix framework.
I would like to use my current implementation of the websocket to proxy the messages from rabbitmq back to the client.
This is the beginning of my attempt. Very early stages, so let me know if I'm going the wrong way, because now would be the time to change the approach.
First some context:
The websocket actor communicates with another actor that holds information about the sockets and the rooms.
pub struct WSConn {
pub id: Uuid,
room_id: Uuid,
hb: Instant,
lobby_address: Addr<Lobby>,
}
impl WSConn {
pub fn new(lobby: Addr<Lobby>, rabbit: Addr<MyRabbit>) -> Self {
Self {
id: Uuid::new_v4(),
room_id: Uuid::nil(),
hb: Instant::now(),
lobby_address: lobby,
}
}
fn hb(&self, context: &mut WebsocketContext<Self>) {
/// HEARTBEAT CODE GOES HERE ///
}
}
So, when the actor starts, I send a connect message to the Lobby, that handles all the logic for adding the connection to the Lobby struct.
impl Actor for WSConn {
type Context = WebsocketContext<Self>;
fn started(&mut self, ctx: &mut Self::Context) {
info!("Starting hearbeat");
self.hb(ctx);
let wsserver_address = ctx.address();
info!("A new client has connected with id {}", self.id);
self.lobby_address.send(Connect {
address: wsserver_address.recipient(),
room_id: self.room_id,
id: self.id
}).into_actor(self).then(|res, _, ctx| {
match res {
Ok(_) => (),
_ => ctx.stop()
}
fut::ready(())
}).wait(ctx);
}
fn stopping(&mut self, _ctx: &mut Self::Context) -> Running {
info!("Stopping actor");
self.lobby_address.do_send(Disconnect {
room_id: self.room_id,
id: self.id
});
Running::Stop
}
}
Here's what the lobby looks like:
type Socket = Recipient<WSServerMessage>;
pub struct Lobby {
pub sessions: DashMap<Uuid, Socket>,//self id to self
pub rooms: DashMap<Uuid, DashSet<Uuid>>,//room id to list of users id
}
The entire Lobby code is quite big, so I won't put it here. Let me know if you need to see that and I'll provide the code.
So, once the client connects, it gets assigned to the default room. When a client sends a message to the server, the message gets processed by the StreamHandler.
impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for WSConn {
fn handle(&mut self, item: Result<Message, ProtocolError>, ctx: &mut Self::Context) {
match item.unwrap() {
ws::Message::Binary(bin) => ctx.binary(bin),
ws::Message::Ping(bin) => {
self.hb = Instant::now();
ctx.pong(&bin);
}
ws::Message::Pong(_) => self.hb = Instant::now(),
ws::Message::Close(reason) => {
ctx.close(reason);
ctx.stop();
}
ws::Message::Text(text) => {
let command = serde_json::from_str::<Command>(&text)
.expect(&format!("Can't parse message {}", &text));
info!("{:?}", command);
if command.command.starts_with("/") {
info!("This is a {} request", command.command);
match command.command.as_ref() {
"/join" => {
info!("Join Room {}", command.payload);
let uid = Uuid::from_str(command.payload.as_str().unwrap()).expect("Can't parse message {} to uuid");
self.lobby_address.send(Join {
current_room: self.room_id,
room_id: uid,
id: self.id
}).into_actor(self).then(|res, _, ctx| {
match res {
Ok(_) => (),
_ => ctx.stop()
}
fut::ready(())
}).wait(ctx);
self.room_id = uid;
},
_ => ()
}
} else {
info!("Text is {}", text);
self.lobby_address.do_send(ClientActorMessage {
id: self.id,
msg: command,
room_id: self.room_id,
});
}
}
_ => {
info!("Something weird happened. Closing");
self.lobby_address.do_send(Disconnect {
room_id: self.room_id,
id: self.id
});
ctx.stop();}
}
}
}
So, as you can see, if you send a message with the command /join and a payload with a valid uuidv4, you join the room. I'm only allowing the client to be a part of one room at a time. So, when you join one, you're automatically removed from the last one.
Ok, so now let's talk about the rabbitmq connection.
The way I thought about this was to use a connection pool to keep the rabbitmq connection and use that connection to create the channels.
So I started by defining the struct that will hold my connection pool.
use actix::{Actor, Context, Handler, StreamHandler};
use deadpool_lapin::{Config, Pool, Runtime};
use deadpool_lapin::lapin::Error;
use lapin::message::Delivery;
use crate::lapin_server::messages::CreateChannel;
pub struct MyRabbit {
pub pool: Pool,
}
impl MyRabbit {
pub fn new() -> Self {
let mut cfg = Config::default();
cfg.url = Some("amqps://ghqcmhat:KbhPAA309QRg7TjdgFEV14pQRheoh44P#codfish.rmq.cloudamqp.com/ghqcmhat".into());
let new_pool = cfg.create_pool(Some(Runtime::Tokio1)).expect("Can't create pool");
MyRabbit {
pool: new_pool
}
}
}
impl Actor for MyRabbit {
type Context = Context<Self>;
}
Having this as an actor allows me to start the actor as I start the websocket server. This is done in main.
#[actix_web::main]
async fn main() -> std::io::Result<()>{
std::env::set_var("RUST_LOG", "actix_web=info,info");
env_logger::init();
// Start Lobby actor and get his address
let websocket_lobby = Lobby::default().start();
let rabbit = MyRabbit::new().start();
let application_data = web::Data::new(Appdata::new());
info!("Starting server on 127.0.0.1:8080");
let server = HttpServer::new(move || {
App::new()
.wrap(Logger::default())
.route("/ws/",web::get().to(websocket_handler))
.app_data(Data::new(websocket_lobby.clone()))
.app_data(Data::new(rabbit.clone()))
.app_data(application_data.clone())
});
server.bind("127.0.0.1:8080")?.run().await
}
To accommodate for the new actor, I added a new parameter to my request handler.
pub async fn websocket_handler(request: HttpRequest, stream: web::Payload, srv: Data<Addr<Lobby>>, rab: Data<Addr<MyRabbit>>, data: Data<Appdata>) -> Result<HttpResponse, Error> {
let mut counter = data.counter.lock().unwrap();
counter.add_assign(1);
info!("This is request # {}", counter);
let ws = WSConn::new(srv.get_ref().clone(), rab.get_ref().clone());
let response = ws::start(ws, &request, stream);
debug!("Response: {:?}", &response);
response
}
Now, my WSConn struct looks like this.
pub struct WSConn {
pub id: Uuid,
room_id: Uuid,
hb: Instant,
lobby_address: Addr<Lobby>,
rabbit_address: Addr<MyRabbit>
}
I know that, if I want to consume from a topic in rabbitmq, I need the exchange name, the type, the routing key and the queue name.
So, I put these in a struct as well
pub struct Channel {
pub queue_name: String,
pub exchange_name: String,
pub exchange_type: ExchangeKind,
pub routing_key: String
}
impl Default for Channel {
fn default() -> Self {
Self {
queue_name: "".to_string(),
exchange_name: "".to_string(),
exchange_type: Default::default(),
routing_key: "".to_string()
}
}
}
But that's where I'm stuck. First, I'm not sure this deadpool_lapin is the right crate to use for this. I'm also not sure how to translate the example on lapin's page, which uses
async_global_executor::block_on
And spawns new threads using async_global_executor::spawn to consume messages.
So, again, what I want is to be able to proxy messages coming from the websocket to rabbitmq and vice versa.
So, if a client connects to the websocket and sends a message like:
{
command: "SUBSCRIBE"
payload: "topic_name"
}
The result should be that messages published on that topic will get sent to him.
Sending an UNSUBSCRIBE should undo that.
Any help here would be greatly appreciated.
Please let me know if more information is needed.
Thank you
Fabricio

Rust Actix Actor send message to actor

How to send a message to another actor?
pub struct MyWs {
}
impl Actor for MyWs {
type Context = ws::WebsocketContext<Self>;
}
impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for MyWs {
fn handle(&mut self, msg: Result<ws::Message, ws::ProtocolError>, ctx: &mut Self::Context) {
match msg {
Ok(ws::Message::Ping(msg)) => ctx.pong(&msg),
Ok(ws::Message::Text(message)) => {
//considering that here he sent the message to self
ctx.text(message);
//how to do something like this
//find the actor by index (or uuid) and send text
//actors[0].text(message);
//
},
Ok(ws::Message::Binary(bin)) => ctx.binary(bin),
Ok(ws::Message::Close(reason)) => ctx.close(reason),
_ => (),
}
}
}
#[get("/ws")]
pub async fn websocket(req: HttpRequest, stream: web::Payload,) -> actix_web::Result<HttpResponse> {
let resp = ws::start(
MyWs {},
&req,
stream,
);
return resp;
}
Could I make a hashMap of actors?
pub struct MyWs { sessions: HashMap<Uuid, Socket> }
and later
self.sessions.text(message)
I'm new to rust and I don't see a way to save the socket (the context or actor) to find it and send it messages.
You might want to check the official example of a chat room app using actix web.
https://github.com/actix/examples/blob/743af0ff1a9be6fb1cc13e6583108463c89ded4d/websockets/chat/src/main.rs
There are three key points:
create another server actor and get the address of it.
let server = server::ChatServer::new(app_state.clone()).start();
set the server actor's address as application data of HttpServer.
HttpServer::new(move || {
App::new()
.data(app_state.clone())
// copy server actor's address into app
.data(server.clone())
.service(web::resource("/").route(web::get().to(|| {
HttpResponse::Found()
.header("LOCATION", "/static/websocket.html")
.finish()
})))
.route("/count/", web::get().to(get_count))
// websocket
.service(web::resource("/ws/").to(chat_route))
// static resources
.service(fs::Files::new("/static/", "static/"))
})
.bind("127.0.0.1:8080")?
.run()
.await
store the address while starting websocket actor in function chat_route. And then you could use the address to send messages whenever you want
/// Entry point for our websocket route
async fn chat_route(
req: HttpRequest,
stream: web::Payload,
srv: web::Data<Addr<server::ChatServer>>,
) -> Result<HttpResponse, Error> {
ws::start(
WsChatSession {
id: 0,
hb: Instant::now(),
room: "Main".to_owned(),
name: None,
addr: srv.get_ref().clone(),
},
&req,
stream,
)
}

How to expand session.Value with String

Iam using Gorilla/Sessions.
I got a template page, where the user can choose between different devices.
If he uses one of the submit buttons under each device my controller function should add the id value to my existing session value.
func Cart(w http.ResponseWriter, r *http.Request) {
data := CartData{
Name: "Cart",
Equipment: model.GetEquipment(model.Db),
Pages: []Page{
{
Title: "Meine Geräte",
Active: false,
Link: "/my-equipment",
},
{
Title: "Equipment",
Active: false,
Link: "/equipment",
},
{
Title: "Logout",
Active: false,
Link: "/logout",
},
},
}
equipment,_ := model.GetEquipmentByID(r.FormValue("id"))
session, _ := store.Get(r, "cookie-name")
Strings := strconv.Itoa(equipment.ID)
fmt.Println(Strings)
StringsWithComma := Strings + ","
session.Values["EquipmentIDs"] = session.Values["EquipmentIDs"] + StringsWithComma // THIS CODE LINE DOES NOT WORK, I want to expand "EquipmentIDs" with the new ID
tmpl:= template.Must(template.ParseFiles("template/base_user.html", "template/cart.html"))
tmpl.ExecuteTemplate(w, "base", data)
}
}
Example: User is visiting my Device page. He uses the submit button with id=2
SessionValue["EquipmentIDs"] should be = "2" right now.
After that the User is visiting the Device Page again an uses the submit button with id=6.
Now the SessionValue should be = "2,6"
I am attached to the problem all day and can not get any further
If you have Questions or want to see other parts of my code feel free to ask
Thanks in advance
I thought that you should get original equipment value from the session, combine the equipment ID that user clicks on and save into session for the next request.
I update your code as following:
// ...
equipment, _ := model.GetEquipmentByID(r.FormValue("id"))
session, _ := store.Get(r, "cookie-name")
equipmentIdsStr := strconv.Itoa(equipment.ID)
if original, exist := session.Values["EquipmentIDs"]; exist {
equipmentIdsStr = fmt.Sprintf("%v,%v", original, equipmentIdsStr)
}
session.Values["EquipmentIDs"] = equipmentIdsStr
//You have to save your updated session value
session.Save(r, w)
// ...
Hope this help.

Resources