I am very new to blockchain development and I had a small project I needed to finish last week. Understanding these concepts was the biggest blocker I had as Web3js documentation wasn't helpful.
Writing a smart contract in Solidity uses a design pattern called Publish-Subscribe(PubSub). In simple terms, when a particular event happens, it is broadcasted publicly across the blockchain. Anyone interested in the information can carefully listen for a particular event broadcast and use the information for whatever they need it for.
Was that confusing? Let me tell you a very short story.
In a small village, whenever a new child is given birth into a Family, the town crier must announce this information across the whole village. The census organization records the new addition to the village. Other willing families in the village send their congratulations
Blockchain: The village
SmartContract: The Family,
Event: new child is given birth,
Publisher: The town crier
Subscribers: census organization, Other willing families
Now, lets read the story one more time
In an ethereum blockchain, when an event is emitted in the smart contract, the event is stored in transaction logs and logs are stored on the blockchain. Anyone who is interested in the event can search for past events recorded or subscribe to new ones.
This is also known as EventDriven PubSub Design
Now that you understand what EventDriven PubSub Design is, let's move forward to how this is implemented in solidity and how it helps us understand events and topics.
Using the above analogy, a family smart contract will look like this...
contract Family {
// ... other family logic
event NewChild(person indexed father, person indexed mother, person child);
function giveBirth() public {
//...give Bith Logic
emit NewChild(father, mother, child);
};
}
A NewChild
event is declared and emitted using the emit
keyword
Now anybody that wants can listen for NewChild
event can subscribe to the particular event across the blockchain.
But How?
Using web3, this is a simple code snippet used to subscribe to all event in the blockchain logs
const subscription = web3.eth.subscribe(
'logs',
{
address: '0x123456..',
from: 0,
},
(error, result) => {
if (error) return;
// do something with the data
console.log(result);
}
);
The above code literally listens to all events in the blockchain and that's a lot, tons of data we probably don't need. What we want is to only subscribe to a specific event which is NewChild
Web3 got us covered, web3.eth.subscribe
's second argument is an object with the following keys: fromBlock
, address
, and topics
. My focus will be on topics
because I feel the other two are self-explanatory and I don't want to deviate too much from this article's subject.
So what the Hecks are Topics?
Topics were my worst nightmare because I couldn't find a documentation/article to explain it to me in a way a beginner like me would understand.
According to Web3 docs:
- Topics - Array: An array of values which must each appear in the log entries. The order is important, if you want to leave topics out use null, e.g. [null, '0x00...']. You can also pass another array for each topic with options for that topic e.g. [null, ['option1', 'option2']]
Translation:
Topics are 32-byte (256 bit) βwordsβ that are used to describe whatβs going on in an event.
Topics are what helps us filter the events in the logs. With the right topics, we can subscribe only to a particular event instead of all events.
Events emitted are translated to topics.
Example: Event Transfer
emitted is equal to the topics
below
contract SomeErc20Contract {
// ...some smartcontract logic
event Transfer(address indexed from, address indexed to, uint256 tokens);
function transfer(address to, uint256 tokens) public returns (bool success) {
// some subtraction and addition logic
emit Transfer(
'0x4dc8f417d4eb731d179a0f08b1feaf25216cefd0'
'0xd2b2fb39b10cd50cab7aa8e834879069ab1a8d4',
200
);
}
}
const topics = [
'0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
'0x0000000000000000000000004dc8f417d4eb731d179a0f08b1feaf25216cefd0',
'0x0000000000000000000000000d2b2fb39b10cd50cab7aa8e834879069ab1a8d4'
]
Let me explain how topics
are generated from Event
.
1. Event signature (topics[0]
): The first element in the topics
array is the hash of the event signature. The event hash is generated by stripping off keywords and arguments from the event declaration and then hashed using keccak256(or sha3).
Example:
event Transfer(address indexed from, address indexed to, uint256 tokens);
- Strip off keywords and arguments
'Transfer(address,address,uint256)'
- Hash it using keccak256(or sha3)
keccak256('Transfer(address,address,uint256)');
and we get our first topic(hash of the event signature ) 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
2. Other topics (topics[1 and above]
): are the indexed
arguments of the event. In our event declaration, only the first two arguments are indexed
.
Does that mean 0x4dc8f417d4eb731d179a0f08b1feaf25216cefd0
and 0xd2b2fb39b10cd50cab7aa8e834879069ab1a8d4
are topic[1] and topic[2] respectively? No!
Why? An ethereum address is 20 byte which is 44 characters long but a topic
is 32 byte which is 62 characters long.
To convert a 20 byte string to 32 byte, I wrote a line javascript statement that pads the 44 characters to 62 characters appropriately
const my32byteString = `${web3.utils.padLeft('0x', 24)}${walletAddress.split('0x')[1]}`;
Hence, our topic[1] and topic[2] are
0x0000000000000000000000004dc8f417d4eb731d179a0f08b1feaf25216cefd0
and 0x0000000000000000000000000d2b2fb39b10cd50cab7aa8e834879069ab1a8d4
respectively.
One more Example
We have a smart contract for investment that emits Invested
event whenever an investment has been made
contract Business {
// ... other business logic
event Invested(address indexed who, address indexed into, unit256 amount);
function makeInvestment(address into, uint256 amount) public {
//...some investment logic
emit Invested(msg.sender, into, amount);
};
}
1. Generate the first event signature(topic[0]
) of Invested
event
Solution:
const eventSignature = web3.utils.sha3('Invested(address,address,uint256)');
2. Generate the topics
of an Invested
event made from/by 0x9d1a9d53bbccf2b2c1a4d3612dbe8b8b9bb4a558
Solution:
const fromAddress = '0x9d1a9d53bbccf2b2c1a4d3612dbe8b8b9bb4a558';
const eventSignature = web3.utils.sha3('Invested(address,address,uint256)');
const topics = [
eventSignature,
`${web3.utils.padLeft('0x', 24)}${fromAddress.split('0x')[1]}`,
];
3. Generate the topics
of an Invested
event made to 0xE701CD3329057AeA9D54300DdD05e41b8D74727A
Solution:
const toAddress = '0xE701CD3329057AeA9D54300DdD05e41b8D74727A';
const eventSignature = web3.utils.sha3('Invested(address,address,uint256)');
const topics = [
eventSignature,
null, // the order is important, this belongs to the first index argument and should be set to null if you want to leave it out
`${web3.utils.padLeft('0x', 24)}${toAddress.split('0x')[1]}`,
];
4. Generate the topics
of an Invested
event made from/by 0x9d1a9d53bbccf2b2c1a4d3612dbe8b8b9bb4a558
to 0xE701CD3329057AeA9D54300DdD05e41b8D74727A
Solution:
const fromAddress = '0x9d1a9d53bbccf2b2c1a4d3612dbe8b8b9bb4a558';
const toAddress = '0xE701CD3329057AeA9D54300DdD05e41b8D74727A';
const eventSignature = web3.utils.sha3('Invested(address,address,uint256)');
const topics = [
eventSignature,
`${web3.utils.padLeft('0x', 24)}${fromAddress.split('0x'),
`${web3.utils.padLeft('0x', 24)}${toAddress.split('0x')[1]}`,
];
Top comments (4)
Oh my god! Thanks for existing to cover this topic my dude
Hey could you explain the difference between
eth.subscribe
andmyContract.getPastEvents
?I know getPastEvents is to a 10,000 blocks range, is that also the case on subscribe ?
Thanks.
Thank you so much!
I could not find a clearer explanation in all of Stack Exchange π
Finally a greate answer, thank you man